@esportsplus/routing 0.6.0

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/.editorconfig ADDED
@@ -0,0 +1,9 @@
1
+ root = true
2
+
3
+ [*]
4
+ indent_style = space
5
+ indent_size = 4
6
+ charset = utf-8
7
+ trim_trailing_whitespace = true
8
+ insert_final_newline = true
9
+ end_of_line = lf
package/.gitattributes ADDED
@@ -0,0 +1,2 @@
1
+ # Auto detect text files and perform LF normalization
2
+ * text=auto
@@ -0,0 +1,25 @@
1
+ # To get started with Dependabot version updates, you'll need to specify which
2
+ # package ecosystems to update and where the package manifests are located.
3
+ # Please see the documentation for all configuration options:
4
+ # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
5
+
6
+ version: 2
7
+
8
+ registries:
9
+ npm-npmjs:
10
+ token: ${{secrets.NPM_TOKEN}}
11
+ type: npm-registry
12
+ url: https://registry.npmjs.org
13
+
14
+ updates:
15
+ - package-ecosystem: "npm"
16
+ directory: "/"
17
+ groups:
18
+ production-dependencies:
19
+ dependency-type: "production"
20
+ development-dependencies:
21
+ dependency-type: "development"
22
+ registries:
23
+ - npm-npmjs
24
+ schedule:
25
+ interval: "daily"
@@ -0,0 +1,9 @@
1
+ name: bump version
2
+
3
+ on:
4
+ push:
5
+ branches: '**' # only trigger on branches, not on tags
6
+
7
+ jobs:
8
+ bump:
9
+ uses: esportsplus/typescript/.github/workflows/bump.yml@main
@@ -0,0 +1,12 @@
1
+ name: dependabot automerge
2
+
3
+ on:
4
+ pull_request:
5
+ types: [opened, synchronize, labeled]
6
+ workflow_dispatch:
7
+
8
+ jobs:
9
+ automerge:
10
+ secrets:
11
+ NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
12
+ uses: esportsplus/typescript/.github/workflows/dependabot.yml@main
@@ -0,0 +1,16 @@
1
+ name: publish to npm
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+ workflow_dispatch:
7
+ workflow_run:
8
+ workflows: [bump version]
9
+ types:
10
+ - completed
11
+
12
+ jobs:
13
+ publish:
14
+ secrets:
15
+ NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
16
+ uses: esportsplus/typescript/.github/workflows/publish.yml@main
package/README.md ADDED
@@ -0,0 +1,217 @@
1
+ # @esportsplus/routing
2
+
3
+ Type-safe client-side router with radix tree matching, middleware pipelines, and reactive navigation.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pnpm add @esportsplus/routing
9
+ ```
10
+
11
+ ## Features
12
+
13
+ - Type-safe route names and path parameters
14
+ - Radix tree matching (static > params > wildcards)
15
+ - Composable middleware pipeline
16
+ - Reactive navigation via `@esportsplus/reactivity`
17
+ - Named routes with URI generation
18
+ - Route factories for modular definitions
19
+ - Subdomain routing
20
+ - HTTP method routing (GET, POST, PUT, DELETE)
21
+
22
+ ## Usage
23
+
24
+ ### Define Routes
25
+
26
+ ```typescript
27
+ import { router, Middleware, Next, Request, Route, RouteFactory } from '@esportsplus/routing/client';
28
+
29
+ type Response = HTMLElement;
30
+
31
+ // Route factory for modular definitions
32
+ const homeRoutes: RouteFactory<Response> = (r) => r
33
+ .get({
34
+ name: 'home',
35
+ path: '/',
36
+ responder: (req) => renderHome()
37
+ })
38
+ .get({
39
+ name: 'about',
40
+ path: '/about',
41
+ responder: (req) => renderAbout()
42
+ });
43
+
44
+ const userRoutes: RouteFactory<Response> = (r) => r
45
+ .get({
46
+ name: 'user',
47
+ path: '/users/:id',
48
+ responder: (req) => renderUser(req.data.parameters?.id)
49
+ })
50
+ .get({
51
+ name: 'user.settings',
52
+ path: '/users/:id/settings',
53
+ middleware: [authMiddleware],
54
+ responder: (req) => renderSettings(req.data.parameters?.id)
55
+ });
56
+ ```
57
+
58
+ ### Create Router
59
+
60
+ ```typescript
61
+ // Compose route factories
62
+ const app = router(homeRoutes, userRoutes);
63
+
64
+ // Navigate
65
+ app.redirect('home');
66
+ app.redirect('user', { id: 123 });
67
+
68
+ // Generate URIs
69
+ app.uri('user', { id: 456 }); // '#/users/456'
70
+
71
+ // History navigation
72
+ app.back();
73
+ app.forward();
74
+ ```
75
+
76
+ ### Middleware
77
+
78
+ ```typescript
79
+ const authMiddleware: Middleware<Response> = (req, next) => {
80
+ if (!isAuthenticated()) {
81
+ return renderLogin();
82
+ }
83
+ return next(req);
84
+ };
85
+
86
+ const loggerMiddleware: Middleware<Response> = (req, next) => {
87
+ console.log(`${req.method} ${req.path}`);
88
+ return next(req);
89
+ };
90
+
91
+ // Apply global middleware and dispatch
92
+ app.middleware(loggerMiddleware).dispatch;
93
+ ```
94
+
95
+ ### Reactive Matching
96
+
97
+ ```typescript
98
+ // Create fallback route
99
+ const notFound: Route<Response> = {
100
+ name: 'not-found',
101
+ path: null,
102
+ pipeline: pipeline<Request<Response>, Response>(),
103
+ subdomain: null
104
+ };
105
+
106
+ // Middleware that reactively matches routes
107
+ const matchMiddleware = app.middleware.match(notFound);
108
+
109
+ // Compose and dispatch
110
+ app.middleware(matchMiddleware, loggerMiddleware).dispatch;
111
+ ```
112
+
113
+ ### Route Groups
114
+
115
+ ```typescript
116
+ const apiRoutes: RouteFactory<Response> = (r) => r
117
+ .group({
118
+ path: '/api/v1',
119
+ middleware: [apiAuth]
120
+ })
121
+ .routes((r) => r
122
+ .get({
123
+ name: 'api.users',
124
+ path: '/users',
125
+ responder: handleUsers
126
+ })
127
+ .post({
128
+ name: 'api.users.create',
129
+ path: '/users',
130
+ responder: handleCreateUser
131
+ })
132
+ );
133
+ ```
134
+
135
+ ### Path Parameters
136
+
137
+ ```typescript
138
+ // Required parameter
139
+ .get({ name: 'user', path: '/users/:id', responder })
140
+
141
+ // Optional parameter (prefix with ?)
142
+ .get({ name: 'archive', path: '/posts/?:year/?:month', responder })
143
+
144
+ // Wildcard (captures rest of path)
145
+ .get({ name: 'files', path: '/files/*:path', responder })
146
+ ```
147
+
148
+ ### Subdomain Routing
149
+
150
+ ```typescript
151
+ const adminRoutes: RouteFactory<Response> = (r) => r
152
+ .get({
153
+ name: 'admin.dashboard',
154
+ path: '/dashboard',
155
+ subdomain: 'admin',
156
+ responder: renderAdminDashboard
157
+ });
158
+ ```
159
+
160
+ ## Types
161
+
162
+ ```typescript
163
+ // Route factory function
164
+ type RouteFactory<T> = (router: Router<T, any>) => Router<T, RouteRegistry>;
165
+
166
+ // Middleware function
167
+ type Middleware<T> = (input: Request<T>, next: Next<T>) => T;
168
+
169
+ // Next function in middleware chain
170
+ type Next<T> = (input: Request<T>) => T;
171
+
172
+ // Request object
173
+ type Request<T> = {
174
+ data: Record<PropertyKey, unknown> & { parameters?: Record<string, unknown>; route?: Route<T> };
175
+ hostname: string;
176
+ href: string;
177
+ method: string;
178
+ origin: string;
179
+ path: string;
180
+ port: string;
181
+ protocol: string;
182
+ query: Record<string, unknown>;
183
+ subdomain?: string;
184
+ };
185
+
186
+ // Route definition
187
+ type Route<T> = {
188
+ name: string | null;
189
+ path: string | null;
190
+ pipeline: Pipeline<Request<T>, T>;
191
+ subdomain: string | null;
192
+ };
193
+ ```
194
+
195
+ ## Route Matching Priority
196
+
197
+ 1. **Static paths** - exact match (`/users`)
198
+ 2. **Parameters** - dynamic segments (`/users/:id`)
199
+ 3. **Wildcards** - catch-all (`/files/*:path`)
200
+
201
+ Static paths always take precedence over parameterized paths for the same position.
202
+
203
+ ## Hash-Based Navigation
204
+
205
+ Routes use hash-based URLs (`#/path`) for client-side navigation without server configuration.
206
+
207
+ ```typescript
208
+ // URL: https://example.com/#/users/123?tab=profile
209
+
210
+ request.path // '/users/123'
211
+ request.query // { tab: 'profile' }
212
+ request.hostname // 'example.com'
213
+ ```
214
+
215
+ ## License
216
+
217
+ MIT
@@ -0,0 +1,9 @@
1
+ declare const ON_DELETE: string[];
2
+ declare const ON_GET: string[];
3
+ declare const ON_POST: string[];
4
+ declare const ON_PUT: string[];
5
+ declare const PARAMETER = 0;
6
+ declare const STATIC = 1;
7
+ declare const WILDCARD = 2;
8
+ export { ON_DELETE, ON_GET, ON_POST, ON_PUT, PARAMETER, STATIC, WILDCARD };
9
+ export { PACKAGE_NAME } from '../constants.js';
@@ -0,0 +1,9 @@
1
+ const ON_DELETE = ['DELETE'];
2
+ const ON_GET = ['GET'];
3
+ const ON_POST = ['POST'];
4
+ const ON_PUT = ['PUT'];
5
+ const PARAMETER = 0;
6
+ const STATIC = 1;
7
+ const WILDCARD = 2;
8
+ export { ON_DELETE, ON_GET, ON_POST, ON_PUT, PARAMETER, STATIC, WILDCARD };
9
+ export { PACKAGE_NAME } from '../constants.js';
@@ -0,0 +1,17 @@
1
+ import { AccumulateRoutes, ExtractOptionalParams, ExtractRequiredParams, InferOutput, Middleware, Next, PathParamsObject, Request, Route, RouteFactory, RoutePath } from './types.js';
2
+ declare function back(): void;
3
+ declare function forward(): void;
4
+ declare const router: <const Factories extends readonly RouteFactory<any>[]>(...factories: Factories) => {
5
+ back: typeof back;
6
+ forward: typeof forward;
7
+ middleware: {
8
+ (...stages: Middleware<InferOutput<Factories[number]>>[]): InferOutput<Factories[number]>;
9
+ dispatch(request: Request<InferOutput<Factories[number]>>): InferOutput<Factories[number]>;
10
+ match(fallback: Route<InferOutput<Factories[number]>>): (request: Request<InferOutput<Factories[number]>>, next: Next<InferOutput<Factories[number]>>) => InferOutput<Factories[number]>;
11
+ };
12
+ redirect: <RouteName extends keyof AccumulateRoutes<Factories>>(name: RouteName, ...values: ExtractRequiredParams<RoutePath<AccumulateRoutes<Factories>, RouteName>> extends never ? ExtractOptionalParams<RoutePath<AccumulateRoutes<Factories>, RouteName>> extends never ? [] : [params?: PathParamsObject<RoutePath<AccumulateRoutes<Factories>, RouteName>>] : [params: PathParamsObject<RoutePath<AccumulateRoutes<Factories>, RouteName>>]) => void;
13
+ routes: Record<string, Route<InferOutput<Factories[number]>>>;
14
+ uri: <RouteName extends keyof AccumulateRoutes<Factories>>(name: RouteName, ...values: ExtractRequiredParams<RoutePath<AccumulateRoutes<Factories>, RouteName>> extends never ? ExtractOptionalParams<RoutePath<AccumulateRoutes<Factories>, RouteName>> extends never ? [] : [params?: PathParamsObject<RoutePath<AccumulateRoutes<Factories>, RouteName>>] : [params: PathParamsObject<RoutePath<AccumulateRoutes<Factories>, RouteName>>]) => string;
15
+ };
16
+ export { router };
17
+ export type { Middleware, Next, Request, Route, RouteFactory } from './types.js';
@@ -0,0 +1,143 @@
1
+ import * as reactivity_fa31f85817714b2e94bba1bba5e367420 from '@esportsplus/reactivity';
2
+ import { effect, root } from '@esportsplus/reactivity';
3
+ import { Router } from './router/index.js';
4
+ import pipeline from '@esportsplus/pipeline';
5
+ import { PACKAGE_NAME } from './constants.js';
6
+ class ReactiveObject_fa31f85817714b2e94bba1bba5e367421 extends reactivity_fa31f85817714b2e94bba1bba5e367420.ReactiveObject {
7
+ #parameters;
8
+ #route;
9
+ constructor(_p0, _p1) {
10
+ super(null);
11
+ this.#parameters = this[reactivity_fa31f85817714b2e94bba1bba5e367420.SIGNAL](_p0);
12
+ this.#route = this[reactivity_fa31f85817714b2e94bba1bba5e367420.SIGNAL](_p1);
13
+ }
14
+ get parameters() {
15
+ return reactivity_fa31f85817714b2e94bba1bba5e367420.read(this.#parameters);
16
+ }
17
+ set parameters(_v0) {
18
+ reactivity_fa31f85817714b2e94bba1bba5e367420.write(this.#parameters, _v0);
19
+ }
20
+ get route() {
21
+ return reactivity_fa31f85817714b2e94bba1bba5e367420.read(this.#route);
22
+ }
23
+ set route(_v1) {
24
+ reactivity_fa31f85817714b2e94bba1bba5e367420.write(this.#route, _v1);
25
+ }
26
+ }
27
+ let cache = [], location = window.location;
28
+ function back() {
29
+ window.history.back();
30
+ }
31
+ function forward() {
32
+ window.history.forward();
33
+ }
34
+ function href() {
35
+ let hash = location.hash || '#/', path = hash ? hash.slice(1).split('?') : ['/', ''], request = {
36
+ hostname: location.hostname,
37
+ href: location.href,
38
+ method: 'GET',
39
+ origin: location.origin,
40
+ path: path[0],
41
+ port: location.port,
42
+ protocol: location.protocol,
43
+ query: {}
44
+ };
45
+ if (path[1]) {
46
+ let params = new URLSearchParams(path[1]), query = request.query;
47
+ for (let [key, value] of params.entries()) {
48
+ query[key] = value;
49
+ }
50
+ }
51
+ return request;
52
+ }
53
+ function match(request, router, subdomain) {
54
+ if (router.subdomains !== null) {
55
+ let hostname = request.hostname, subdomains = router.subdomains;
56
+ for (let i = 0, n = subdomains.length; i < n; i++) {
57
+ if (!hostname.startsWith(subdomains[i])) {
58
+ continue;
59
+ }
60
+ subdomain = subdomains[i];
61
+ break;
62
+ }
63
+ }
64
+ return router.match(request.method, request.path, subdomain || '');
65
+ }
66
+ function middleware(request, router) {
67
+ let middleware = pipeline();
68
+ function host(...stages) {
69
+ for (let i = 0, n = stages.length; i < n; i++) {
70
+ middleware.add(stages[i]);
71
+ }
72
+ return middleware.dispatch(request);
73
+ }
74
+ ;
75
+ host.dispatch = (request) => {
76
+ let { route } = request.data;
77
+ if (route === undefined) {
78
+ throw new Error(`${PACKAGE_NAME}: route is undefined!`);
79
+ }
80
+ return route.pipeline.dispatch(request);
81
+ };
82
+ host.match = (fallback) => {
83
+ let state = new ReactiveObject_fa31f85817714b2e94bba1bba5e367421(undefined, undefined);
84
+ if (fallback === undefined) {
85
+ throw new Error(`${PACKAGE_NAME}: fallback route does not exist`);
86
+ }
87
+ effect(() => {
88
+ let { parameters, route } = match(request, router);
89
+ state.parameters = parameters;
90
+ state.route = route || fallback;
91
+ });
92
+ return (request, next) => {
93
+ if (state.route === undefined) {
94
+ throw new Error(`${PACKAGE_NAME}: route is undefined`);
95
+ }
96
+ return root(() => {
97
+ request.data = {
98
+ parameters: state.parameters,
99
+ route: state.route
100
+ };
101
+ return next(request);
102
+ });
103
+ };
104
+ };
105
+ return host;
106
+ }
107
+ function normalize(uri) {
108
+ if (uri[0] === '/') {
109
+ return '#' + uri;
110
+ }
111
+ return uri;
112
+ }
113
+ function onpopstate() {
114
+ let values = href();
115
+ for (let i = 0, n = cache.length; i < n; i++) {
116
+ let state = cache[i];
117
+ for (let key in values) {
118
+ state[key] = values[key];
119
+ }
120
+ }
121
+ }
122
+ const router = (...factories) => {
123
+ let instance = factories.reduce((r, factory) => factory(r), new Router()), request = reactivity_fa31f85817714b2e94bba1bba5e367420.reactive(Object.assign(href(), { data: {} }));
124
+ if (cache.push(request) === 1) {
125
+ window.addEventListener('hashchange', onpopstate);
126
+ }
127
+ return {
128
+ back,
129
+ forward,
130
+ middleware: middleware(request, instance),
131
+ redirect: (name, ...values) => {
132
+ if (name.indexOf('://') !== -1) {
133
+ return window.location.replace(name);
134
+ }
135
+ window.location.hash = normalize(instance.uri(name, values));
136
+ },
137
+ routes: instance.routes,
138
+ uri: (name, ...values) => {
139
+ return normalize(instance.uri(name, values));
140
+ }
141
+ };
142
+ };
143
+ export { router };
@@ -0,0 +1,64 @@
1
+ import { Route, Name, Options, PathParamsTuple, RouteOptions, RouteRegistry } from '../types.js';
2
+ import { Node } from './node.js';
3
+ declare function key(method: string, subdomain?: string | null): string;
4
+ declare class Router<T, TRoutes extends RouteRegistry = {}> {
5
+ bucket: Record<ReturnType<typeof key>, {
6
+ root: Node<T>;
7
+ static: Record<string, Route<T>>;
8
+ }>;
9
+ groups: Options<T>[];
10
+ routes: Record<Name, Route<T>>;
11
+ subdomains: string[] | null;
12
+ private add;
13
+ private create;
14
+ delete<RouteName extends string = string, RoutePath extends string = string>(options: RouteOptions<T> & {
15
+ name?: RouteName;
16
+ path?: RoutePath;
17
+ }): Router<T, TRoutes & (RouteName extends string ? RoutePath extends string ? {
18
+ [K in RouteName]: {
19
+ path: RoutePath;
20
+ };
21
+ } : TRoutes : TRoutes)>;
22
+ get<RouteName extends string = string, RoutePath extends string = string>(options: RouteOptions<T> & {
23
+ name?: RouteName;
24
+ path?: RoutePath;
25
+ }): Router<T, TRoutes & (RouteName extends string ? RoutePath extends string ? {
26
+ [K in RouteName]: {
27
+ path: RoutePath;
28
+ };
29
+ } : TRoutes : TRoutes)>;
30
+ group(options: Options<T>): {
31
+ routes: (fn: (router: Router<T, TRoutes>) => void) => Router<T, TRoutes>;
32
+ };
33
+ match(method: string, path: string, subdomain?: string | null): {
34
+ parameters?: Readonly<Record<PropertyKey, unknown>>;
35
+ route?: Readonly<Route<T>> | undefined;
36
+ };
37
+ on<RouteName extends string = string, RoutePath extends string = string>(methods: string[], options: RouteOptions<T> & {
38
+ name?: RouteName;
39
+ path?: RoutePath;
40
+ }): Router<T, TRoutes & (RouteName extends string ? RoutePath extends string ? {
41
+ [K in RouteName]: {
42
+ path: RoutePath;
43
+ };
44
+ } : TRoutes : TRoutes)>;
45
+ post<RouteName extends string = string, RoutePath extends string = string>(options: RouteOptions<T> & {
46
+ name?: RouteName;
47
+ path?: RoutePath;
48
+ }): Router<T, TRoutes & (RouteName extends string ? RoutePath extends string ? {
49
+ [K in RouteName]: {
50
+ path: RoutePath;
51
+ };
52
+ } : TRoutes : TRoutes)>;
53
+ put<RouteName extends string = string, RoutePath extends string = string>(options: RouteOptions<T> & {
54
+ name?: RouteName;
55
+ path?: RoutePath;
56
+ }): Router<T, TRoutes & (RouteName extends string ? RoutePath extends string ? {
57
+ [K in RouteName]: {
58
+ path: RoutePath;
59
+ };
60
+ } : TRoutes : TRoutes)>;
61
+ uri<RouteName extends keyof TRoutes & string>(name: RouteName, values?: PathParamsTuple<TRoutes[RouteName]['path']>): string;
62
+ }
63
+ export { Router };
64
+ export type { Route };