Skip to content
Open
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
8 changes: 7 additions & 1 deletion src/routing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { runtimeDir } from "nitro/meta";
import { addRoute, createRouter, findRoute, findAllRoutes } from "rou3";
import { compileRouterToString } from "rou3/compiler";
import { hash } from "ohash";
import { encodeNonAsciiRoute } from "./utils/route.ts";

const isGlobalMiddleware = (h: NitroEventHandler) => !h.method && (!h.route || h.route === "/**");

Expand Down Expand Up @@ -157,7 +158,12 @@ export class Router<T> {
this._router = createRouter<T>();
this._compiled = undefined;
for (const route of routes) {
addRoute(this._router, route.method, this._baseURL + route.route, route.data);
addRoute(
this._router,
route.method,
this._baseURL + encodeNonAsciiRoute(route.route),
route.data
);
}
if (opts?.merge) {
mergeCatchAll(this._router);
Expand Down
16 changes: 16 additions & 0 deletions src/utils/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Percent-encodes the non-ASCII characters in a route pattern, leaving the rest untouched.
export function encodeNonAsciiRoute(route: string): string {
let hasNonAscii = false;
for (let i = 0; i < route.length; i++) {
if (route.charCodeAt(i) > 127) {
hasNonAscii = true;
break;
}
}
if (!hasNonAscii) {
return route;
}
return Array.from(route)
.map((char) => (char.codePointAt(0)! > 127 ? encodeURIComponent(char) : char))
.join("");
}
9 changes: 9 additions & 0 deletions test/vite/nonascii-route-fixture/nitro.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { defineConfig } from "nitro";

export default defineConfig({
preset: "static",
prerender: { routes: ["/に぀いて"] },
// Points to an ASCII-named handler file; the route pattern itself is the literal
// non-ASCII text.
handlers: [{ route: "/に぀いて", handler: "./routes/nonascii.ts", method: "GET" }],
});
1 change: 1 addition & 0 deletions test/vite/nonascii-route-fixture/routes/nonascii.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default () => "<h1>ok</h1>";
6 changes: 6 additions & 0 deletions test/vite/nonascii-route-fixture/vite.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { defineConfig } from "vite";
import { nitro } from "nitro/vite";

export default defineConfig({
plugins: [nitro()],
});
63 changes: 63 additions & 0 deletions test/vite/nonascii-route.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { fileURLToPath } from "node:url";
import { join } from "node:path";
import { rm, mkdir, readFile } from "node:fs/promises";
import { describe, expect, it } from "vitest";
import { createNitro, build, prepare } from "nitro/builder";

const fixtureDir = fileURLToPath(new URL("./nonascii-route-fixture", import.meta.url));
const tmpDir = fileURLToPath(new URL("./nonascii-route-fixture/.tmp", import.meta.url));

// A route path written in Japanese. Registered via `handlers` in the fixture's
// nitro.config.ts so the route pattern stays literal Unicode without needing a
// non-ASCII filename in the repo.
const route = "/に぀いて";

describe("non-ASCII route paths", () => {
it("is prerendered under its own literal path (static preset)", async () => {
const outDir = join(tmpDir, "static");
await rm(outDir, { recursive: true, force: true });
await mkdir(outDir, { recursive: true });
const nitro = await createNitro({
rootDir: fixtureDir,
preset: "static",
output: { dir: outDir },
builder: "vite",
});
try {
await prepare(nitro);
await build(nitro);
} finally {
await nitro.close();
}

// Reads the file prerendered at the route's own literal path.
const html = await readFile(join(outDir, "public", "に぀いて"), "utf8");
expect(html).toBe("<h1>ok</h1>");
}, 30_000);

it("is reachable on a running server", async () => {
const outDir = join(tmpDir, "standard");
await rm(outDir, { recursive: true, force: true });
await mkdir(outDir, { recursive: true });
const nitro = await createNitro({
rootDir: fixtureDir,
preset: "standard",
output: { dir: outDir },
builder: "vite",
});
try {
await prepare(nitro);
await build(nitro);
} finally {
await nitro.close();
}

const entry = join(outDir, "server/index.mjs");
const { fetch } = await import(entry).then((m) => m.default);

// Requests the literal Unicode path, exactly as a browser navigation would send it.
const response = await fetch(new Request(`http://localhost${route}`));
expect(response.status).toBe(200);
expect(await response.text()).toBe("<h1>ok</h1>");
}, 30_000);
});