Skip to content
Draft
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
32 changes: 1 addition & 31 deletions packages/void/src/plugin-framework-entry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ export function getOriginalFrameworkEntry(
case 'vinext-app':
return 'vinext/server/app-router-entry';
case 'vinext-pages':
return generateVinextPagesEntry(root);
return 'vinext/server/pages-router-entry';
}
}

Expand Down Expand Up @@ -173,36 +173,6 @@ ${isBuild ? `${compileAssetFallbackHelper(assetsDir, false, notFound)}\n` : ''}`
return wrapperPath;
}

/** Generate a worker entry for vinext Pages Router projects. */
function generateVinextPagesEntry(root: string): string {
const outDir = join(root, '.void');
mkdirSync(outDir, { recursive: true });
const entryPath = join(outDir, 'vinext-pages-entry.ts');
writeFileIfChanged(
entryPath,
`// Auto-generated by Void — vinext Pages Router worker entry
// @ts-expect-error virtual module resolved by vinext at build time
import { renderPage, handleApiRoute } from "virtual:vinext-server-entry";

export default {
async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
const pathname = url.pathname;
if (pathname.replaceAll("\\\\", "/").startsWith("//")) {
return new Response("404 Not Found", { status: 404 });
}
const urlWithQuery = pathname + url.search;
if (pathname.startsWith("/api/") || pathname === "/api") {
return await handleApiRoute(request, urlWithQuery);
}
return await renderPage(request, urlWithQuery, null);
},
};
`,
);
return entryPath;
}

/** Generate a worker entry for React Router v7 projects. */
function generateReactRouterEntry(root: string): string {
const outDir = join(root, '.void');
Expand Down
124 changes: 122 additions & 2 deletions packages/void/test/integration/vinext-pages.test.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,50 @@
import { readFileSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import spawn from 'cross-spawn';
import { afterAll, beforeAll, describe, expect, it } from 'vite-plus/test';
import { killChild, startServer } from '../../../../test/integration-helpers.ts';
import { Miniflare, convertV4MiniflareOptions } from 'miniflare';
import { afterAll, afterEach, beforeAll, describe, expect, it } from 'vite-plus/test';
import {
createIsolatedPlayground,
killChild,
startServer,
type IsolatedPlayground,
} from '../../../../test/integration-helpers.ts';
import { collectWorkerFiles } from '../../src/cli/package.ts';

const ROOT = join(import.meta.dirname, '..', '..', '..', '..');
const PLAYGROUND = 'playground/vinext-pages';
let DEV_BASE: string;

/** The response surface shared by `fetch` and Miniflare's `dispatchFetch`. */
type Fetch = (path: string) => Promise<{
status: number;
headers: { get(name: string): string | null };
text(): Promise<string>;
}>;

/**
* The playground's `middleware.ts` and `next.config.mjs` each add a redirect, a
* rewrite and a response header. They run only when the worker goes through vinext's own
* Pages Router request pipeline.
*/
function expectNextRequestPipeline(fetchPath: Fetch) {
return Promise.all([
...['/', '/middleware-rewrite', '/config-rewrite'].map(async (path) => {
const response = await fetchPath(path);
const html = await response.text();
expect(response.status, `${path}: ${html.slice(0, 500)}`).toBe(200);
expect(html, path).toContain('vinext Pages Router + Void');
expect(response.headers.get('x-void-middleware'), path).toBe('1');
expect(response.headers.get('x-void-config'), path).toBe('1');
}),
...['/middleware-redirect', '/config-redirect'].map(async (path) => {
const response = await fetchPath(path);
expect(response.status, path).toBe(307);
expect(new URL(response.headers.get('location')!, 'http://localhost').pathname).toBe('/');
}),
]);
}

describe('playground/vinext-pages dev', () => {
let child: ReturnType<typeof spawn>;

Expand Down Expand Up @@ -59,4 +97,86 @@ describe('playground/vinext-pages dev', () => {
expect(html).toContain('Alice');
expect(html).toContain('ok');
});

it('runs middleware and next.config redirects, rewrites and headers', async () => {
await expectNextRequestPipeline((path) => fetch(`${DEV_BASE}${path}`, { redirect: 'manual' }));
});
});

describe('playground/vinext-pages build', () => {
const playgrounds: Array<IsolatedPlayground> = [];
let runtime: Miniflare | undefined;

afterEach(async () => {
await runtime?.dispose();
runtime = undefined;
});

afterAll(() => {
for (const playground of playgrounds.splice(0)) {
playground.cleanup();
}
});

it.each([['voidPlugin(), vinext()'], ['vinext(), voidPlugin()']])(
'builds a worker that runs the Next.js request pipeline with plugins [%s]',
async (order) => {
const playground = createIsolatedPlayground(ROOT, PLAYGROUND);
playgrounds.push(playground);
writeFileSync(
join(playground.root, 'vite.config.ts'),
`import vinext from 'vinext';
import { defineConfig } from 'vite-plus';
import { voidPlugin } from 'void';

export default defineConfig({
plugins: [${order}],
});
`,
);
const result = await new Promise<{ status: number | null; output: string }>(
(resolve, reject) => {
const child = spawn('vp', ['build'], {
cwd: playground.root,
env: { ...process.env, VOID_INSPECTOR_PORT: 'false' },
stdio: ['ignore', 'pipe', 'pipe'],
});
let output = '';
child.stdout!.on('data', (chunk: Buffer) => (output += String(chunk)));
child.stderr!.on('data', (chunk: Buffer) => (output += String(chunk)));
child.on('error', reject);
child.on('close', (status) => resolve({ status, output }));
},
);
expect(result.status, result.output).toBe(0);

const serverDir = join(playground.root, 'dist', 'ssr');
const config = JSON.parse(readFileSync(join(serverDir, 'wrangler.json'), 'utf8'));
const files = collectWorkerFiles(serverDir);
const modulePaths = [
config.main,
...Array.from(files.keys()).filter((path) => path !== config.main),
];
runtime = new Miniflare(
convertV4MiniflareOptions({
compatibilityDate: config.compatibility_date,
compatibilityFlags: config.compatibility_flags,
modules: modulePaths
.filter((path) => /\.(?:js|mjs)$/.test(path))
.map((path) => ({
type: 'ESModule',
path: join(serverDir, path),
contents: files.get(path)!,
})),
modulesRoot: serverDir,
d1Databases: ['DB'],
kvNamespaces: ['KV'],
}),
);
await expectNextRequestPipeline((path) =>
runtime!.dispatchFetch(`http://localhost${path}`, { redirect: 'manual' }),
);
},
180_000,
);
});
8 changes: 8 additions & 0 deletions packages/void/test/unit/plugin-class-a-wrapper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,14 @@ describe('Class A framework custom entry', () => {
},
);

it.each([
['vinext-app', 'vinext/server/app-router-entry'],
// vinext's own entry runs middleware and next.config redirects, rewrites and headers.
['vinext-pages', 'vinext/server/pages-router-entry'],
] as const)('uses vinext’s own worker entry for %s', (framework, entry) => {
expect(getOriginalFrameworkEntry(framework, createTanStackRoot())).toBe(entry);
});

it('keeps the default TanStack server entry when no custom entry is configured', () => {
expect(getOriginalFrameworkEntry('tanstack-start', createTanStackRoot())).toBe(
'@tanstack/react-start/server-entry',
Expand Down
15 changes: 15 additions & 0 deletions playground/vinext-pages/middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { NextResponse, type NextRequest } from 'next/server';

export function middleware(request: NextRequest) {
if (request.nextUrl.pathname === '/middleware-redirect') {
return NextResponse.redirect(new URL('/', request.url));
}
const response =
request.nextUrl.pathname === '/middleware-rewrite'
? NextResponse.rewrite(new URL('/', request.url))
: NextResponse.next();
response.headers.set('x-void-middleware', '1');
return response;
}

export const config = { matcher: ['/((?!_next).*)'] };
11 changes: 11 additions & 0 deletions playground/vinext-pages/next.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export default {
async redirects() {
return [{ source: '/config-redirect', destination: '/', permanent: false }];
},
async rewrites() {
return [{ source: '/config-rewrite', destination: '/' }];
},
async headers() {
return [{ source: '/:path*', headers: [{ key: 'x-void-config', value: '1' }] }];
},
};
Loading