mirror of
https://github.com/cloudflare/vinext.git
synced 2026-09-14 19:04:59 +08:00
97947ce775
* fix(app-router): apply trailingSlash to route handler request URL (#1827) App Router route handlers received a NextRequest whose `url` was normalized with the default `trailingSlash: false` policy, so a request to `/runtime/edge/` (with `trailingSlash: true`) surfaced `req.url` without the trailing slash even though `req.nextUrl.pathname` kept it. Thread the configured `trailingSlash` through the route-handler dispatch chain (dispatch -> execution/cache -> tracked request -> NextRequest) so `NextURL` applies the correct policy when computing `request.url`, matching Next.js (test/e2e/app-dir/app-routes-trailing-slash). Closes #1827 * test(app-router): cover node runtime for trailingSlash:false route handler (#1827) Address ask-bonk review note on PR #1835: the trailingSlash:false suite only exercised the edge runtime. Parameterize it over edge + node to match the trailingSlash:true suite's coverage.
101 lines
3.5 KiB
TypeScript
101 lines
3.5 KiB
TypeScript
/**
|
||
* Regression coverage for issue #1827 — `trailingSlash` not applied to App
|
||
* Router route handlers (`app/**/route.ts`).
|
||
*
|
||
* Mirrors Next.js test/e2e/app-dir/app-routes-trailing-slash:
|
||
* - With `trailingSlash: true`, a request to `/runtime/edge` returns
|
||
* 308 → `/runtime/edge/`
|
||
* - The redirected `/runtime/edge/` request returns 200 and the handler
|
||
* observes the trailing-slash pathname.
|
||
*
|
||
* Refs cloudflare/vinext#1827
|
||
*/
|
||
import fs from "node:fs";
|
||
import os from "node:os";
|
||
import path from "node:path";
|
||
import { afterAll, beforeAll, describe, expect, it } from "vite-plus/test";
|
||
import type { ViteDevServer } from "vite-plus";
|
||
import { APP_FIXTURE_DIR, startFixtureServer } from "./helpers.js";
|
||
|
||
function copyAppFixtureWithTrailingSlash(prefix: string, trailingSlash: boolean): string {
|
||
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
||
fs.cpSync(APP_FIXTURE_DIR, tmpDir, { recursive: true });
|
||
fs.rmSync(path.join(tmpDir, "node_modules", ".vite"), { recursive: true, force: true });
|
||
fs.writeFileSync(
|
||
path.join(tmpDir, "next.config.ts"),
|
||
`import type { NextConfig } from "vinext";
|
||
const nextConfig: NextConfig = { trailingSlash: ${trailingSlash} };
|
||
export default nextConfig;
|
||
`,
|
||
);
|
||
return tmpDir;
|
||
}
|
||
|
||
describe("App Router route-handler trailingSlash: true (#1827)", () => {
|
||
let tmpDir: string;
|
||
let server: ViteDevServer;
|
||
let baseUrl: string;
|
||
|
||
beforeAll(async () => {
|
||
tmpDir = copyAppFixtureWithTrailingSlash("vinext-route-ts-true-", true);
|
||
({ server, baseUrl } = await startFixtureServer(tmpDir));
|
||
}, 60000);
|
||
|
||
afterAll(async () => {
|
||
await server?.close();
|
||
if (tmpDir) fs.rmSync(tmpDir, { recursive: true, force: true });
|
||
});
|
||
|
||
it.each(["edge", "node"])(
|
||
"redirects /runtime/%s with 308 and serves the slashed path",
|
||
async (runtime) => {
|
||
const res = await fetch(`${baseUrl}/runtime/${runtime}`, { redirect: "manual" });
|
||
expect(res.status).toBe(308);
|
||
const location = res.headers.get("location");
|
||
expect(location).not.toBeNull();
|
||
expect(new URL(location!, baseUrl).pathname).toBe(`/runtime/${runtime}/`);
|
||
|
||
const slashed = await fetch(`${baseUrl}/runtime/${runtime}/`, { redirect: "manual" });
|
||
expect(slashed.status).toBe(200);
|
||
await expect(slashed.json()).resolves.toEqual({
|
||
url: `/runtime/${runtime}/`,
|
||
nextUrl: `/runtime/${runtime}/`,
|
||
});
|
||
},
|
||
30000,
|
||
);
|
||
});
|
||
|
||
describe("App Router route-handler trailingSlash: false (#1827)", () => {
|
||
let tmpDir: string;
|
||
let server: ViteDevServer;
|
||
let baseUrl: string;
|
||
|
||
beforeAll(async () => {
|
||
tmpDir = copyAppFixtureWithTrailingSlash("vinext-route-ts-false-", false);
|
||
({ server, baseUrl } = await startFixtureServer(tmpDir));
|
||
}, 60000);
|
||
|
||
afterAll(async () => {
|
||
await server?.close();
|
||
if (tmpDir) fs.rmSync(tmpDir, { recursive: true, force: true });
|
||
});
|
||
|
||
it.each(["edge", "node"])(
|
||
"redirects /runtime/%s/ with 308 and serves the unslashed path",
|
||
async (runtime) => {
|
||
const res = await fetch(`${baseUrl}/runtime/${runtime}/`, { redirect: "manual" });
|
||
expect(res.status).toBe(308);
|
||
expect(new URL(res.headers.get("location")!, baseUrl).pathname).toBe(`/runtime/${runtime}`);
|
||
|
||
const unslashed = await fetch(`${baseUrl}/runtime/${runtime}`, { redirect: "manual" });
|
||
expect(unslashed.status).toBe(200);
|
||
await expect(unslashed.json()).resolves.toEqual({
|
||
url: `/runtime/${runtime}`,
|
||
nextUrl: `/runtime/${runtime}`,
|
||
});
|
||
},
|
||
30000,
|
||
);
|
||
});
|