@jlnstack/routes 0.0.0 → 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/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
- }
@@ -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
- });
@@ -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
@@ -1,5 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.json",
3
- "include": ["src", "test"],
4
- "compilerOptions": {}
5
- }
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
- });
package/vitest.config.ts DELETED
@@ -1,10 +0,0 @@
1
- import { defineConfig } from "vitest/config";
2
-
3
- export default defineConfig({
4
- test: {
5
- exclude: ["node_modules", "dist"],
6
- typecheck: {
7
- enabled: true,
8
- },
9
- },
10
- });