release: WRNexusJS 0.8.0
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@wrnexus/router",
|
||||
"version": "0.7.0",
|
||||
"version": "0.8.0",
|
||||
"type": "module",
|
||||
"main": "src/index.ts",
|
||||
"exports": {
|
||||
|
||||
@@ -4,10 +4,12 @@
|
||||
*/
|
||||
|
||||
import { getRouteParams, type Route } from "./match.ts";
|
||||
import { nameRoutes } from "./named.ts";
|
||||
|
||||
export function generateRoutesFile(pages: Route[]): string {
|
||||
const seen = new Set<string>();
|
||||
const entries: string[] = [];
|
||||
const namedEntries: string[] = [];
|
||||
for (const page of [...pages].sort((a, b) => a.raw.localeCompare(b.raw))) {
|
||||
if (seen.has(page.raw)) continue;
|
||||
seen.add(page.raw);
|
||||
@@ -23,6 +25,8 @@ export function generateRoutesFile(pages: Route[]): string {
|
||||
: "Record<string, never>";
|
||||
entries.push(` ${JSON.stringify(page.raw)}: ${type};`);
|
||||
}
|
||||
for (const page of nameRoutes([...pages].sort((a, b) => a.raw.localeCompare(b.raw))))
|
||||
namedEntries.push(` ${JSON.stringify(page.name)}: ${JSON.stringify(page.raw)};`);
|
||||
|
||||
return `// AUTO-GENERATED by \`wrnexus dev\` - do not edit.
|
||||
// Typed routes support required, optional, and catch-all parameters.
|
||||
@@ -31,7 +35,19 @@ export interface Routes {
|
||||
${entries.join("\n") || " [path: string]: Record<string, string>;"}
|
||||
}
|
||||
|
||||
export interface RouteNames {
|
||||
${namedEntries.join("\n") || " [name: string]: string;"}
|
||||
}
|
||||
|
||||
export interface RouteQueries {
|
||||
[path: string]: Record<string, string | number | boolean | null | undefined>;
|
||||
}
|
||||
|
||||
export type RoutePath = keyof Routes;
|
||||
export type RouteName = keyof RouteNames;
|
||||
export type RouteQuery<P extends RoutePath> = P extends keyof RouteQueries
|
||||
? RouteQueries[P]
|
||||
: Record<string, string | number | boolean | null | undefined>;
|
||||
type RouteValue = string | readonly string[] | undefined;
|
||||
|
||||
function encodeRouteValue(value: RouteValue, catchAll: boolean): string {
|
||||
@@ -40,17 +56,9 @@ function encodeRouteValue(value: RouteValue, catchAll: boolean): string {
|
||||
return values.map((part) => encodeURIComponent(part)).join("/");
|
||||
}
|
||||
|
||||
export function href<P extends RoutePath>(
|
||||
path: P,
|
||||
...args: keyof Routes[P] extends never
|
||||
? []
|
||||
: Record<string, never> extends Routes[P]
|
||||
? [params?: Routes[P]]
|
||||
: [params: Routes[P]]
|
||||
): string {
|
||||
const params = (args[0] ?? {}) as Record<string, RouteValue>;
|
||||
function buildHref(path: string, params: Record<string, RouteValue> = {}): string {
|
||||
const output: string[] = [];
|
||||
for (const segment of String(path).split("/").filter(Boolean)) {
|
||||
for (const segment of path.split("/").filter(Boolean)) {
|
||||
let name: string | undefined;
|
||||
let optional = false;
|
||||
let catchAll = false;
|
||||
@@ -79,5 +87,36 @@ export function href<P extends RoutePath>(
|
||||
}
|
||||
return "/" + output.filter(Boolean).join("/");
|
||||
}
|
||||
|
||||
export function href<P extends RoutePath>(
|
||||
path: P,
|
||||
...args: keyof Routes[P] extends never
|
||||
? []
|
||||
: Record<string, never> extends Routes[P]
|
||||
? [params?: Routes[P]]
|
||||
: [params: Routes[P]]
|
||||
): string {
|
||||
const params = (args[0] ?? {}) as Record<string, RouteValue>;
|
||||
return buildHref(String(path), params);
|
||||
}
|
||||
|
||||
export function route<N extends RouteName>(
|
||||
name: N,
|
||||
...args: keyof Routes[RouteNames[N]] extends never
|
||||
? [params?: Routes[RouteNames[N]], query?: RouteQuery<RouteNames[N]>]
|
||||
: Record<string, never> extends Routes[RouteNames[N]]
|
||||
? [params?: Routes[RouteNames[N]], query?: RouteQuery<RouteNames[N]>]
|
||||
: [params: Routes[RouteNames[N]], query?: RouteQuery<RouteNames[N]>]
|
||||
): string {
|
||||
const paths: Record<RouteName, RoutePath> = ${JSON.stringify(Object.fromEntries(nameRoutes([...pages].sort((a, b) => a.raw.localeCompare(b.raw))).map((page) => [page.name, page.raw])), null, 2)} as Record<RouteName, RoutePath>;
|
||||
const output = buildHref(paths[name], (args[0] ?? {}) as Record<string, RouteValue>);
|
||||
const query = args[1];
|
||||
if (!query) return output;
|
||||
const search = new URLSearchParams();
|
||||
for (const [key, value] of Object.entries(query))
|
||||
if (value !== undefined && value !== null) search.set(key, String(value));
|
||||
const text = search.toString();
|
||||
return text ? \`\${output}?\${text}\` : output;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -25,6 +25,8 @@ test("generateRoutesFile emits a Routes map with param types", () => {
|
||||
expect(code).toContain('"/users/[id]": { "id": string };');
|
||||
expect(code).toContain('"/docs/[[...slug]]": { "slug"?: string | readonly string[] };');
|
||||
expect(code).toContain("export function href");
|
||||
expect(code).toContain('"users.id": "/users/[id]";');
|
||||
expect(code).toContain("export function route");
|
||||
});
|
||||
|
||||
test("generated href() fills required, optional and catch-all params", async () => {
|
||||
@@ -45,6 +47,11 @@ test("generated href() fills required, optional and catch-all params", async ()
|
||||
writeFileSync(file, code);
|
||||
const mod = (await import(pathToFileURL(file).href)) as {
|
||||
href: (p: string, params?: Record<string, string | string[]>) => string;
|
||||
route: (
|
||||
name: string,
|
||||
params?: Record<string, string | string[]>,
|
||||
query?: Record<string, string | number | boolean>,
|
||||
) => string;
|
||||
};
|
||||
expect(mod.href("/")).toBe("/");
|
||||
expect(mod.href("/users/[id]", { id: "42" })).toBe("/users/42");
|
||||
@@ -56,6 +63,9 @@ test("generated href() fills required, optional and catch-all params", async ()
|
||||
expect(mod.href("/docs/[...slug]", { slug: ["guide", "start here"] })).toBe(
|
||||
"/docs/guide/start%20here",
|
||||
);
|
||||
expect(mod.route("users.id", { id: "42" }, { tab: "profile", compact: true })).toBe(
|
||||
"/users/42?tab=profile&compact=true",
|
||||
);
|
||||
});
|
||||
|
||||
test("matches optional and catch-all routes", () => {
|
||||
|
||||
Reference in New Issue
Block a user