@jlnstack/routes 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -1
- package/.turbo/turbo-dev.log +0 -27299
- package/src/index.ts +0 -163
- package/src/proxy.ts +0 -26
- package/test/index.test-d.ts +0 -60
- package/test/index.test.ts +0 -58
- package/tsconfig.json +0 -5
- package/tsdown.config.ts +0 -16
- package/vitest.config.ts +0 -10
package/src/index.ts
DELETED
|
@@ -1,163 +0,0 @@
|
|
|
1
|
-
import { createRecursiveProxy } from "./proxy";
|
|
2
|
-
|
|
3
|
-
type SegmentName<S extends string> = S extends `[...${infer N}]`
|
|
4
|
-
? N
|
|
5
|
-
: S extends `[[...${infer N}]]`
|
|
6
|
-
? N
|
|
7
|
-
: S extends `[${infer N}]`
|
|
8
|
-
? N
|
|
9
|
-
: S;
|
|
10
|
-
|
|
11
|
-
type ChildSegments<
|
|
12
|
-
Routes extends string,
|
|
13
|
-
Path extends string,
|
|
14
|
-
> = Routes extends `${Path}/${infer Next}/${string}`
|
|
15
|
-
? SegmentName<Next>
|
|
16
|
-
: Routes extends `${Path}/${infer Next}`
|
|
17
|
-
? SegmentName<Next>
|
|
18
|
-
: never;
|
|
19
|
-
|
|
20
|
-
type IsDynamicSegment<S extends string> = S extends
|
|
21
|
-
| `[${string}]`
|
|
22
|
-
| `[...${string}]`
|
|
23
|
-
| `[[...${string}]]`
|
|
24
|
-
? true
|
|
25
|
-
: false;
|
|
26
|
-
|
|
27
|
-
type IsCatchAllSegment<S extends string> = S extends `[...${string}]`
|
|
28
|
-
? true
|
|
29
|
-
: false;
|
|
30
|
-
|
|
31
|
-
type IsOptionalCatchAllSegment<S extends string> = S extends `[[...${string}]]`
|
|
32
|
-
? true
|
|
33
|
-
: false;
|
|
34
|
-
|
|
35
|
-
type GetOriginalSegment<
|
|
36
|
-
Routes extends string,
|
|
37
|
-
Path extends string,
|
|
38
|
-
Seg extends string,
|
|
39
|
-
> = Routes extends `${Path}/${infer Next}/${string}`
|
|
40
|
-
? SegmentName<Next> extends Seg
|
|
41
|
-
? Next
|
|
42
|
-
: never
|
|
43
|
-
: Routes extends `${Path}/${infer Next}`
|
|
44
|
-
? SegmentName<Next> extends Seg
|
|
45
|
-
? Next
|
|
46
|
-
: never
|
|
47
|
-
: never;
|
|
48
|
-
|
|
49
|
-
type IsDynamicAtPath<
|
|
50
|
-
Routes extends string,
|
|
51
|
-
Path extends string,
|
|
52
|
-
Seg extends string,
|
|
53
|
-
> = GetOriginalSegment<Routes, Path, Seg> extends infer Original
|
|
54
|
-
? Original extends string
|
|
55
|
-
? IsDynamicSegment<Original> extends true
|
|
56
|
-
? true
|
|
57
|
-
: false
|
|
58
|
-
: false
|
|
59
|
-
: false;
|
|
60
|
-
|
|
61
|
-
type IsCatchAllAtPath<
|
|
62
|
-
Routes extends string,
|
|
63
|
-
Path extends string,
|
|
64
|
-
Seg extends string,
|
|
65
|
-
> = GetOriginalSegment<Routes, Path, Seg> extends infer Original
|
|
66
|
-
? Original extends string
|
|
67
|
-
? IsCatchAllSegment<Original> extends true
|
|
68
|
-
? true
|
|
69
|
-
: false
|
|
70
|
-
: false
|
|
71
|
-
: false;
|
|
72
|
-
|
|
73
|
-
type IsOptionalCatchAllAtPath<
|
|
74
|
-
Routes extends string,
|
|
75
|
-
Path extends string,
|
|
76
|
-
Seg extends string,
|
|
77
|
-
> = GetOriginalSegment<Routes, Path, Seg> extends infer Original
|
|
78
|
-
? Original extends string
|
|
79
|
-
? IsOptionalCatchAllSegment<Original> extends true
|
|
80
|
-
? true
|
|
81
|
-
: false
|
|
82
|
-
: false
|
|
83
|
-
: false;
|
|
84
|
-
|
|
85
|
-
type Join<
|
|
86
|
-
T extends readonly string[],
|
|
87
|
-
Sep extends string,
|
|
88
|
-
> = T extends readonly [
|
|
89
|
-
infer First extends string,
|
|
90
|
-
...infer Rest extends readonly string[],
|
|
91
|
-
]
|
|
92
|
-
? Rest["length"] extends 0
|
|
93
|
-
? First
|
|
94
|
-
: `${First}${Sep}${Join<Rest, Sep>}`
|
|
95
|
-
: "";
|
|
96
|
-
|
|
97
|
-
type ReplaceDynamicSegments<
|
|
98
|
-
Path extends string,
|
|
99
|
-
P extends Record<string, string | string[] | undefined>,
|
|
100
|
-
> = Path extends `${infer Start}/[[...${infer Param}]]${infer Rest}`
|
|
101
|
-
? Param extends keyof P
|
|
102
|
-
? P[Param] extends readonly string[]
|
|
103
|
-
? `${ReplaceDynamicSegments<Start, P>}/${Join<P[Param], "/">}${ReplaceDynamicSegments<Rest, P>}`
|
|
104
|
-
: `${ReplaceDynamicSegments<Start, P>}${ReplaceDynamicSegments<Rest, P>}`
|
|
105
|
-
: `${ReplaceDynamicSegments<Start, P>}${ReplaceDynamicSegments<Rest, P>}`
|
|
106
|
-
: Path extends `${infer Start}/[...${infer Param}]${infer Rest}`
|
|
107
|
-
? Param extends keyof P
|
|
108
|
-
? P[Param] extends readonly string[]
|
|
109
|
-
? `${ReplaceDynamicSegments<Start, P>}/${Join<P[Param], "/">}${ReplaceDynamicSegments<Rest, P>}`
|
|
110
|
-
: `${ReplaceDynamicSegments<Start, P>}/[...${Param}]${ReplaceDynamicSegments<Rest, P>}`
|
|
111
|
-
: `${ReplaceDynamicSegments<Start, P>}/[...${Param}]${ReplaceDynamicSegments<Rest, P>}`
|
|
112
|
-
: Path extends `${infer Start}/[${infer Param}]${infer Rest}`
|
|
113
|
-
? Param extends keyof P
|
|
114
|
-
? P[Param] extends string
|
|
115
|
-
? `${Start}/${P[Param]}${ReplaceDynamicSegments<Rest, P>}`
|
|
116
|
-
: `${Start}/[${Param}]${ReplaceDynamicSegments<Rest, P>}`
|
|
117
|
-
: `${Start}/[${Param}]${ReplaceDynamicSegments<Rest, P>}`
|
|
118
|
-
: Path;
|
|
119
|
-
|
|
120
|
-
export type RouteNode<
|
|
121
|
-
Routes extends string,
|
|
122
|
-
Path extends string,
|
|
123
|
-
Params extends Record<string, string | string[]> = {},
|
|
124
|
-
> = {
|
|
125
|
-
getRoute: [keyof Params] extends [never]
|
|
126
|
-
? () => Path extends "" ? "/" : Path
|
|
127
|
-
: <const P extends Params>(params: P) => ReplaceDynamicSegments<Path, P>;
|
|
128
|
-
} & {
|
|
129
|
-
[Seg in ChildSegments<Routes, Path>]: RouteNode<
|
|
130
|
-
Routes,
|
|
131
|
-
Path extends ""
|
|
132
|
-
? `/${GetOriginalSegment<Routes, Path, Seg>}`
|
|
133
|
-
: `${Path}/${GetOriginalSegment<Routes, Path, Seg>}`,
|
|
134
|
-
IsOptionalCatchAllAtPath<Routes, Path, Seg> extends true
|
|
135
|
-
? Params & { [K in Seg]?: string[] }
|
|
136
|
-
: IsCatchAllAtPath<Routes, Path, Seg> extends true
|
|
137
|
-
? Params & { [K in Seg]: string[] }
|
|
138
|
-
: IsDynamicAtPath<Routes, Path, Seg> extends true
|
|
139
|
-
? Params & { [K in Seg]: string }
|
|
140
|
-
: Params
|
|
141
|
-
>;
|
|
142
|
-
};
|
|
143
|
-
|
|
144
|
-
export function createRoutes<Routes extends string>() {
|
|
145
|
-
return createRecursiveProxy(({ path, args }) => {
|
|
146
|
-
const segments = path.slice(0, -1);
|
|
147
|
-
const params = (args[0] ?? {}) as Record<
|
|
148
|
-
string,
|
|
149
|
-
string | string[] | undefined
|
|
150
|
-
>;
|
|
151
|
-
|
|
152
|
-
if (segments.length === 0) return "/";
|
|
153
|
-
|
|
154
|
-
const resolved = segments.flatMap((seg) => {
|
|
155
|
-
const value = params[seg];
|
|
156
|
-
if (Array.isArray(value)) return value;
|
|
157
|
-
if (value === undefined) return [];
|
|
158
|
-
return value ?? seg;
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
return resolved.length === 0 ? "/" : `/${resolved.join("/")}`;
|
|
162
|
-
}) as RouteNode<Routes, "", {}>;
|
|
163
|
-
}
|
package/src/proxy.ts
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
interface ProxyCallbackOptions {
|
|
2
|
-
path: readonly string[];
|
|
3
|
-
args: readonly unknown[];
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
type ProxyCallback = (opts: ProxyCallbackOptions) => unknown;
|
|
7
|
-
|
|
8
|
-
export function createRecursiveProxy(
|
|
9
|
-
callback: ProxyCallback,
|
|
10
|
-
path: readonly string[] = [],
|
|
11
|
-
) {
|
|
12
|
-
const proxy: unknown = new Proxy(() => {}, {
|
|
13
|
-
get(_obj, key) {
|
|
14
|
-
if (typeof key !== "string") return undefined;
|
|
15
|
-
return createRecursiveProxy(callback, [...path, key]);
|
|
16
|
-
},
|
|
17
|
-
apply(_1, _2, args) {
|
|
18
|
-
return callback({
|
|
19
|
-
path,
|
|
20
|
-
args,
|
|
21
|
-
});
|
|
22
|
-
},
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
return proxy;
|
|
26
|
-
}
|
package/test/index.test-d.ts
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import { expectTypeOf, test } from "vitest";
|
|
2
|
-
import { createRoutes, type RouteNode } from "../src/index";
|
|
3
|
-
|
|
4
|
-
type AppRoutes =
|
|
5
|
-
| "/"
|
|
6
|
-
| "/dashboard"
|
|
7
|
-
| "/dashboard/settings"
|
|
8
|
-
| "/blog/[slug]"
|
|
9
|
-
| "/users/[id]/posts/[postId]"
|
|
10
|
-
| "/docs/[...path]"
|
|
11
|
-
| "/shop/[[...filters]]";
|
|
12
|
-
|
|
13
|
-
const routes = createRoutes<AppRoutes>() as RouteNode<AppRoutes, "", {}>;
|
|
14
|
-
|
|
15
|
-
test("root route returns literal '/'", () => {
|
|
16
|
-
const route = routes.getRoute();
|
|
17
|
-
expectTypeOf(route).toEqualTypeOf<"/">();
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
test("static segment returns literal path", () => {
|
|
21
|
-
const route = routes.dashboard.getRoute();
|
|
22
|
-
expectTypeOf(route).toEqualTypeOf<"/dashboard">();
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
test("nested static segments return literal path", () => {
|
|
26
|
-
const route = routes.dashboard.settings.getRoute();
|
|
27
|
-
expectTypeOf(route).toEqualTypeOf<"/dashboard/settings">();
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
test("dynamic param requires string param", () => {
|
|
31
|
-
const route = routes.blog.slug.getRoute({ slug: "my-article" });
|
|
32
|
-
expectTypeOf(route).toEqualTypeOf<"/blog/my-article">();
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
test("nested dynamic params require all params", () => {
|
|
36
|
-
const route = routes.users.id.posts.postId.getRoute({
|
|
37
|
-
id: "123",
|
|
38
|
-
postId: "456",
|
|
39
|
-
});
|
|
40
|
-
expectTypeOf(route).toEqualTypeOf<"/users/123/posts/456">();
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
test("catch-all requires string array param", () => {
|
|
44
|
-
const route = routes.docs.path.getRoute({
|
|
45
|
-
path: ["api", "reference"],
|
|
46
|
-
});
|
|
47
|
-
expectTypeOf(route).toMatchTypeOf<"/docs/api/reference">();
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
test("optional catch-all accepts string array", () => {
|
|
51
|
-
const route = routes.shop.filters.getRoute({
|
|
52
|
-
filters: ["color", "red"],
|
|
53
|
-
});
|
|
54
|
-
expectTypeOf(route).toMatchTypeOf<"/shop/color/red">();
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
test("optional catch-all accepts undefined", () => {
|
|
58
|
-
const route = routes.shop.filters.getRoute({ filters: undefined });
|
|
59
|
-
expectTypeOf(route).toMatchTypeOf<"/shop">();
|
|
60
|
-
});
|
package/test/index.test.ts
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from "vitest";
|
|
2
|
-
import { createRoutes, type RouteNode } from "../src/index";
|
|
3
|
-
|
|
4
|
-
type AppRoutes =
|
|
5
|
-
| "/"
|
|
6
|
-
| "/dashboard"
|
|
7
|
-
| "/dashboard/settings"
|
|
8
|
-
| "/blog/[slug]"
|
|
9
|
-
| "/users/[id]/posts/[postId]"
|
|
10
|
-
| "/docs/[...path]"
|
|
11
|
-
| "/shop/[[...filters]]";
|
|
12
|
-
|
|
13
|
-
const routes = createRoutes<AppRoutes>() as RouteNode<AppRoutes, "", {}>;
|
|
14
|
-
|
|
15
|
-
describe("createRoutes", () => {
|
|
16
|
-
it("returns root route", () => {
|
|
17
|
-
const route = routes.getRoute();
|
|
18
|
-
expect(route).toBe("/");
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
it("returns static segment", () => {
|
|
22
|
-
const route = routes.dashboard.getRoute();
|
|
23
|
-
expect(route).toBe("/dashboard");
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it("returns nested static segments", () => {
|
|
27
|
-
const route = routes.dashboard.settings.getRoute();
|
|
28
|
-
expect(route).toBe("/dashboard/settings");
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it("returns dynamic param route", () => {
|
|
32
|
-
const route = routes.blog.slug.getRoute({ slug: "my-article" });
|
|
33
|
-
expect(route).toBe("/blog/my-article");
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it("returns nested dynamic params route", () => {
|
|
37
|
-
const route = routes.users.id.posts.postId.getRoute({
|
|
38
|
-
id: "123",
|
|
39
|
-
postId: "456",
|
|
40
|
-
});
|
|
41
|
-
expect(route).toBe("/users/123/posts/456");
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it("returns catch-all route", () => {
|
|
45
|
-
const route = routes.docs.path.getRoute({ path: ["api", "reference"] });
|
|
46
|
-
expect(route).toBe("/docs/api/reference");
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it("returns optional catch-all with value", () => {
|
|
50
|
-
const route = routes.shop.filters.getRoute({ filters: ["color", "red"] });
|
|
51
|
-
expect(route).toBe("/shop/color/red");
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it("returns optional catch-all without value", () => {
|
|
55
|
-
const route = routes.shop.filters.getRoute({ filters: undefined });
|
|
56
|
-
expect(route).toBe("/shop");
|
|
57
|
-
});
|
|
58
|
-
});
|
package/tsconfig.json
DELETED
package/tsdown.config.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { defineConfig } from "tsdown";
|
|
2
|
-
|
|
3
|
-
export default defineConfig({
|
|
4
|
-
target: ["node18", "es2017"],
|
|
5
|
-
entry: ["src/index.ts"],
|
|
6
|
-
dts: {
|
|
7
|
-
sourcemap: true,
|
|
8
|
-
tsconfig: "./tsconfig.json",
|
|
9
|
-
},
|
|
10
|
-
unbundle: true,
|
|
11
|
-
format: ["cjs", "esm"],
|
|
12
|
-
outExtensions: (ctx) => ({
|
|
13
|
-
dts: ctx.format === "cjs" ? ".d.cts" : ".d.mts",
|
|
14
|
-
js: ctx.format === "cjs" ? ".cjs" : ".mjs",
|
|
15
|
-
}),
|
|
16
|
-
});
|