@wavelengthusaf/client-router 0.0.1

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.
Files changed (169) hide show
  1. package/.gitlab-ci.yml +84 -0
  2. package/.husky/pre-commit +38 -0
  3. package/.storybook/main.ts +16 -0
  4. package/.storybook/manager.ts +7 -0
  5. package/.storybook/preview.ts +23 -0
  6. package/.storybook/theme.ts +30 -0
  7. package/.storybook/vitest.setup.ts +7 -0
  8. package/README.md +556 -0
  9. package/dist/elements/RouteElement.d.ts +0 -0
  10. package/dist/elements/RouteElement.d.ts.map +0 -0
  11. package/dist/elements/RouteElement.js +0 -0
  12. package/dist/elements/RouteElement.js.map +0 -0
  13. package/dist/elements/RouterElement.d.ts +0 -0
  14. package/dist/elements/RouterElement.d.ts.map +0 -0
  15. package/dist/elements/RouterElement.js +0 -0
  16. package/dist/elements/RouterElement.js.map +0 -0
  17. package/dist/elements/__tests__/RouteElement.test.d.ts +0 -0
  18. package/dist/elements/__tests__/RouteElement.test.d.ts.map +0 -0
  19. package/dist/elements/__tests__/RouteElement.test.js +77 -0
  20. package/dist/elements/__tests__/RouteElement.test.js.map +0 -0
  21. package/dist/elements/__tests__/RouterElement.test.d.ts +0 -0
  22. package/dist/elements/__tests__/RouterElement.test.d.ts.map +0 -0
  23. package/dist/elements/__tests__/RouterElement.test.js +0 -0
  24. package/dist/elements/__tests__/RouterElement.test.js.map +0 -0
  25. package/dist/index.d.ts +0 -0
  26. package/dist/index.d.ts.map +0 -0
  27. package/dist/index.js +0 -0
  28. package/dist/index.js.map +0 -0
  29. package/dist/models/CompiledRoute.d.ts +0 -0
  30. package/dist/models/CompiledRoute.d.ts.map +0 -0
  31. package/dist/models/CompiledRoute.js +0 -0
  32. package/dist/models/CompiledRoute.js.map +0 -0
  33. package/dist/models/MatchedRoute.d.ts +0 -0
  34. package/dist/models/MatchedRoute.d.ts.map +0 -0
  35. package/dist/models/MatchedRoute.js +0 -0
  36. package/dist/models/MatchedRoute.js.map +0 -0
  37. package/dist/models/RouteComponent.d.ts +0 -0
  38. package/dist/models/RouteComponent.d.ts.map +0 -0
  39. package/dist/models/RouteComponent.js +0 -0
  40. package/dist/models/RouteComponent.js.map +0 -0
  41. package/dist/models/RouteContext.d.ts +0 -0
  42. package/dist/models/RouteContext.d.ts.map +0 -0
  43. package/dist/models/RouteContext.js +0 -0
  44. package/dist/models/RouteContext.js.map +0 -0
  45. package/dist/models/RouteDefinition.d.ts +0 -0
  46. package/dist/models/RouteDefinition.d.ts.map +0 -0
  47. package/dist/models/RouteDefinition.js +0 -0
  48. package/dist/models/RouteDefinition.js.map +0 -0
  49. package/dist/models/RouteGuard.d.ts +0 -0
  50. package/dist/models/RouteGuard.d.ts.map +0 -0
  51. package/dist/models/RouteGuard.js +0 -0
  52. package/dist/models/RouteGuard.js.map +0 -0
  53. package/dist/models/RouteParams.d.ts +0 -0
  54. package/dist/models/RouteParams.d.ts.map +0 -0
  55. package/dist/models/RouteParams.js +0 -0
  56. package/dist/models/RouteParams.js.map +0 -0
  57. package/dist/models/index.d.ts +0 -0
  58. package/dist/models/index.d.ts.map +0 -0
  59. package/dist/models/index.js +0 -0
  60. package/dist/models/index.js.map +0 -0
  61. package/dist/prettier.config.d.ts +0 -0
  62. package/dist/prettier.config.d.ts.map +0 -0
  63. package/dist/router/Router.d.ts +0 -0
  64. package/dist/router/Router.d.ts.map +0 -0
  65. package/dist/router/Router.js +0 -0
  66. package/dist/router/Router.js.map +0 -0
  67. package/dist/router/__tests__/Router.test.d.ts +0 -0
  68. package/dist/router/__tests__/Router.test.d.ts.map +0 -0
  69. package/dist/router/__tests__/Router.test.js +0 -0
  70. package/dist/router/__tests__/Router.test.js.map +0 -0
  71. package/dist/src/elements/RouteElement.d.ts +0 -0
  72. package/dist/src/elements/RouteElement.d.ts.map +0 -0
  73. package/dist/src/elements/RouterElement.d.ts +0 -0
  74. package/dist/src/elements/RouterElement.d.ts.map +0 -0
  75. package/dist/src/elements/__tests__/RouteElement.test.d.ts +0 -0
  76. package/dist/src/elements/__tests__/RouteElement.test.d.ts.map +0 -0
  77. package/dist/src/elements/__tests__/RouterElement.test.d.ts +0 -0
  78. package/dist/src/elements/__tests__/RouterElement.test.d.ts.map +0 -0
  79. package/dist/src/index.d.ts +0 -0
  80. package/dist/src/index.d.ts.map +0 -0
  81. package/dist/src/models/CompiledRoute.d.ts +0 -0
  82. package/dist/src/models/CompiledRoute.d.ts.map +0 -0
  83. package/dist/src/models/MatchedRoute.d.ts +0 -0
  84. package/dist/src/models/MatchedRoute.d.ts.map +0 -0
  85. package/dist/src/models/RouteComponent.d.ts +0 -0
  86. package/dist/src/models/RouteComponent.d.ts.map +0 -0
  87. package/dist/src/models/RouteContext.d.ts +0 -0
  88. package/dist/src/models/RouteContext.d.ts.map +0 -0
  89. package/dist/src/models/RouteDefinition.d.ts +0 -0
  90. package/dist/src/models/RouteDefinition.d.ts.map +0 -0
  91. package/dist/src/models/RouteGuard.d.ts +0 -0
  92. package/dist/src/models/RouteGuard.d.ts.map +0 -0
  93. package/dist/src/models/RouteParams.d.ts +0 -0
  94. package/dist/src/models/RouteParams.d.ts.map +0 -0
  95. package/dist/src/models/index.d.ts +0 -0
  96. package/dist/src/models/index.d.ts.map +0 -0
  97. package/dist/src/router/Router.d.ts +0 -0
  98. package/dist/src/router/Router.d.ts.map +0 -0
  99. package/dist/src/router/__tests__/Router.test.d.ts +0 -0
  100. package/dist/src/router/__tests__/Router.test.d.ts.map +0 -0
  101. package/dist/src/stories/RouterElement.stories.d.ts +0 -0
  102. package/dist/src/stories/RouterElement.stories.d.ts.map +0 -0
  103. package/dist/src/stories/RouterElement.stories.test.d.ts +0 -0
  104. package/dist/src/stories/RouterElement.stories.test.d.ts.map +0 -0
  105. package/dist/src/utils/__tests__/compileRoute.test.d.ts +0 -0
  106. package/dist/src/utils/__tests__/compileRoute.test.d.ts.map +0 -0
  107. package/dist/src/utils/__tests__/matchRoute.test.d.ts +0 -0
  108. package/dist/src/utils/__tests__/matchRoute.test.d.ts.map +0 -0
  109. package/dist/src/utils/compileRoute.d.ts +0 -0
  110. package/dist/src/utils/compileRoute.d.ts.map +0 -0
  111. package/dist/src/utils/index.d.ts +0 -0
  112. package/dist/src/utils/index.d.ts.map +0 -0
  113. package/dist/src/utils/matchRoute.d.ts +0 -0
  114. package/dist/src/utils/matchRoute.d.ts.map +0 -0
  115. package/dist/stories/RouterElement.stories.d.ts +0 -0
  116. package/dist/stories/RouterElement.stories.d.ts.map +0 -0
  117. package/dist/stories/RouterElement.stories.js +0 -0
  118. package/dist/stories/RouterElement.stories.js.map +0 -0
  119. package/dist/utils/__tests__/compileRoute.test.d.ts +0 -0
  120. package/dist/utils/__tests__/compileRoute.test.d.ts.map +0 -0
  121. package/dist/utils/__tests__/compileRoute.test.js +0 -0
  122. package/dist/utils/__tests__/compileRoute.test.js.map +0 -0
  123. package/dist/utils/__tests__/matchRoute.test.d.ts +0 -0
  124. package/dist/utils/__tests__/matchRoute.test.d.ts.map +0 -0
  125. package/dist/utils/__tests__/matchRoute.test.js +0 -0
  126. package/dist/utils/__tests__/matchRoute.test.js.map +0 -0
  127. package/dist/utils/compileRoute.d.ts +0 -0
  128. package/dist/utils/compileRoute.d.ts.map +0 -0
  129. package/dist/utils/compileRoute.js +0 -0
  130. package/dist/utils/compileRoute.js.map +0 -0
  131. package/dist/utils/index.d.ts +0 -0
  132. package/dist/utils/index.d.ts.map +0 -0
  133. package/dist/utils/index.js +0 -0
  134. package/dist/utils/index.js.map +0 -0
  135. package/dist/utils/matchRoute.d.ts +0 -0
  136. package/dist/utils/matchRoute.d.ts.map +0 -0
  137. package/dist/utils/matchRoute.js +0 -0
  138. package/dist/utils/matchRoute.js.map +0 -0
  139. package/eslint.config.mjs +89 -0
  140. package/index.ts +15 -0
  141. package/package.json +54 -0
  142. package/prettier.config.ts +30 -0
  143. package/src/elements/RouteElement.ts +67 -0
  144. package/src/elements/RouterElement.ts +97 -0
  145. package/src/elements/__tests__/RouteElement.test.ts +89 -0
  146. package/src/elements/__tests__/RouterElement.test.ts +253 -0
  147. package/src/index.ts +15 -0
  148. package/src/models/CompiledRoute.ts +7 -0
  149. package/src/models/MatchedRoute.ts +7 -0
  150. package/src/models/RouteComponent.ts +6 -0
  151. package/src/models/RouteContext.ts +7 -0
  152. package/src/models/RouteDefinition.ts +6 -0
  153. package/src/models/RouteGuard.ts +6 -0
  154. package/src/models/RouteParams.ts +3 -0
  155. package/src/models/index.ts +17 -0
  156. package/src/router/Router.ts +225 -0
  157. package/src/router/__tests__/Router.test.ts +464 -0
  158. package/src/stories/RouterElement.stories.test.ts +310 -0
  159. package/src/stories/RouterElement.stories.ts +208 -0
  160. package/src/utils/__tests__/compileRoute.test.ts +97 -0
  161. package/src/utils/__tests__/matchRoute.test.ts +180 -0
  162. package/src/utils/compileRoute.ts +37 -0
  163. package/src/utils/index.ts +4 -0
  164. package/src/utils/matchRoute.ts +37 -0
  165. package/tsconfig.build.json +20 -0
  166. package/tsconfig.json +55 -0
  167. package/vitest.config.ts +34 -0
  168. package/vitest.setup.ts +3 -0
  169. package/vitest.shims.d.ts +1 -0
@@ -0,0 +1,180 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { matchRoute } from '../matchRoute.ts';
3
+
4
+ function route(path: string, paramNames: string[] = []) {
5
+ return {
6
+ routeDefinition: { path },
7
+ pattern: new RegExp(`^${path.replace(':id', '([^/]+)')}$`),
8
+ paramNames,
9
+ };
10
+ }
11
+
12
+ describe('matchRoute', () => {
13
+ describe('basic matching', () => {
14
+ it.each([
15
+ {
16
+ routes: [route('/home')],
17
+ path: '/home',
18
+ expected: '/home',
19
+ },
20
+ {
21
+ routes: [route('/about')],
22
+ path: '/about',
23
+ expected: '/about',
24
+ },
25
+ ])('matches static route %#', ({ routes, path, expected }) => {
26
+ // @ts-expect-error (passing wrong parameters)
27
+ const result = matchRoute(routes, path);
28
+
29
+ expect(result?.routeDefinition.path).toBe(expected);
30
+ });
31
+ });
32
+
33
+ describe('params', () => {
34
+ it('extracts single param', () => {
35
+ const routes = [
36
+ {
37
+ routeDefinition: { path: '/users/:id' },
38
+ pattern: /^\/users\/([^/]+)$/,
39
+ paramNames: ['id'],
40
+ },
41
+ ];
42
+
43
+ // @ts-expect-error (passing wrong parameters)
44
+ const result = matchRoute(routes, '/users/123');
45
+
46
+ expect(result?.routeParams).toEqual({
47
+ id: '123',
48
+ });
49
+ });
50
+
51
+ it('extracts multiple params', () => {
52
+ const routes = [
53
+ {
54
+ routeDefinition: { path: '/a/:x/b/:y' },
55
+ pattern: /^\/a\/([^/]+)\/b\/([^/]+)$/,
56
+ paramNames: ['x', 'y'],
57
+ },
58
+ ];
59
+
60
+ // @ts-expect-error (passing wrong parameters)
61
+ const result = matchRoute(routes, '/a/1/b/2');
62
+
63
+ expect(result?.routeParams).toEqual({
64
+ x: '1',
65
+ y: '2',
66
+ });
67
+ });
68
+ });
69
+
70
+ describe('priority', () => {
71
+ it('returns first matching route', () => {
72
+ const routes = [
73
+ {
74
+ routeDefinition: { path: '/users' },
75
+ pattern: /^\/users$/,
76
+ paramNames: [],
77
+ },
78
+ {
79
+ routeDefinition: { path: '/users/:id' },
80
+ pattern: /^\/users\/([^/]+)$/,
81
+ paramNames: ['id'],
82
+ },
83
+ ];
84
+
85
+ // @ts-expect-error (passing wrong parameters)
86
+ const result = matchRoute(routes, '/users');
87
+
88
+ expect(result?.routeDefinition.path).toBe('/users');
89
+ });
90
+ });
91
+
92
+ describe('no match', () => {
93
+ it('returns null when no routes match', () => {
94
+ const routes = [
95
+ {
96
+ routeDefinition: { path: '/a' },
97
+ pattern: /^\/a$/,
98
+ paramNames: [],
99
+ },
100
+ ];
101
+
102
+ // @ts-expect-error (passing wrong parameters)
103
+ const result = matchRoute(routes, '/b');
104
+
105
+ expect(result).toBeNull();
106
+ });
107
+ });
108
+
109
+ describe('decodeURIComponent', () => {
110
+ it('decodes encoded values', () => {
111
+ const routes = [
112
+ {
113
+ routeDefinition: { path: '/search/:query' },
114
+ pattern: /^\/search\/([^/]+)$/,
115
+ paramNames: ['query'],
116
+ },
117
+ ];
118
+
119
+ // @ts-expect-error (passing wrong parameters)
120
+ const result = matchRoute(routes, '/search/hello%20world');
121
+
122
+ expect(result?.routeParams.query).toBe('hello world');
123
+ });
124
+ });
125
+
126
+ describe('undefined capture safety', () => {
127
+ it('skips param assignment when capture group is undefined', () => {
128
+ const routes = [
129
+ {
130
+ routeDefinition: { path: '/test/:id/:missing' },
131
+ pattern: /^\/test\/([^/]+)$/,
132
+ paramNames: ['id', 'missing'],
133
+ },
134
+ ];
135
+
136
+ // @ts-expect-error (passing wrong parameters)
137
+ const result = matchRoute(routes, '/test/123');
138
+
139
+ expect(result).not.toBeNull();
140
+ expect(result!.routeParams).toEqual({
141
+ id: '123',
142
+ // missing should NOT exist
143
+ });
144
+ });
145
+ });
146
+
147
+ describe('malformed encoding', () => {
148
+ it('does not throw on bad encoding', () => {
149
+ const routes = [
150
+ {
151
+ routeDefinition: { path: '/test/:id' },
152
+ pattern: /^\/test\/([^/]+)$/,
153
+ paramNames: ['id'],
154
+ },
155
+ ];
156
+
157
+ // @ts-expect-error (passing wrong parameters)
158
+ const bad = () => matchRoute(routes, '/test/%E0%A4%A');
159
+
160
+ expect(bad).not.toThrow();
161
+ });
162
+ });
163
+
164
+ describe('strict matching', () => {
165
+ it('does not match partial paths', () => {
166
+ const routes = [
167
+ {
168
+ routeDefinition: { path: '/users/:id' },
169
+ pattern: /^\/users\/([^/]+)$/,
170
+ paramNames: ['id'],
171
+ },
172
+ ];
173
+
174
+ // @ts-expect-error (passing wrong parameters)
175
+ expect(matchRoute(routes, '/users/123/extra')).toBeNull();
176
+ // @ts-expect-error (passing wrong parameters)
177
+ expect(matchRoute(routes, '/users')).toBeNull();
178
+ });
179
+ });
180
+ });
@@ -0,0 +1,37 @@
1
+ import type { CompiledRoute, RouteDefinition } from '../models/index.ts';
2
+
3
+ function escapeRegex(str: string): string {
4
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
5
+ }
6
+
7
+ export function compileRoute(routeDefinition: RouteDefinition): CompiledRoute {
8
+ const paramNames: string[] = [];
9
+
10
+ if (routeDefinition.path === '*') {
11
+ return {
12
+ routeDefinition,
13
+ pattern: /^.*$/,
14
+ paramNames,
15
+ };
16
+ }
17
+
18
+ const pattern = routeDefinition.path
19
+ .split('/')
20
+ .map((segment) => {
21
+ const match = /^:([a-zA-Z_]\w*)$/.exec(segment);
22
+
23
+ if (match) {
24
+ paramNames.push(match[1]!);
25
+ return '([^/]+)';
26
+ }
27
+
28
+ return escapeRegex(segment);
29
+ })
30
+ .join('\\/');
31
+
32
+ return {
33
+ routeDefinition,
34
+ pattern: new RegExp(`^${pattern}$`),
35
+ paramNames,
36
+ };
37
+ }
@@ -0,0 +1,4 @@
1
+ import { compileRoute } from './compileRoute.ts';
2
+ import { matchRoute } from './matchRoute.ts';
3
+
4
+ export { compileRoute, matchRoute };
@@ -0,0 +1,37 @@
1
+ import type {
2
+ CompiledRoute,
3
+ MatchedRoute,
4
+ RouteParams,
5
+ } from '../models/index.ts';
6
+
7
+ export function matchRoute(
8
+ compiledRoutes: CompiledRoute[],
9
+ pathname: string,
10
+ ): MatchedRoute | null {
11
+ for (const compiledRoute of compiledRoutes) {
12
+ const match: RegExpMatchArray | null = compiledRoute.pattern.exec(pathname);
13
+
14
+ if (!match) continue;
15
+
16
+ const routeParams: RouteParams = {};
17
+
18
+ compiledRoute.paramNames.forEach((name, i) => {
19
+ const value: string | undefined = match[i + 1];
20
+
21
+ if (value === undefined) return;
22
+
23
+ try {
24
+ routeParams[name] = decodeURIComponent(value);
25
+ } catch {
26
+ routeParams[name] = value;
27
+ }
28
+ });
29
+
30
+ return {
31
+ routeDefinition: compiledRoute.routeDefinition,
32
+ routeParams,
33
+ };
34
+ }
35
+
36
+ return null;
37
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "declaration": true,
5
+ "declarationMap": true,
6
+ "emitDeclarationOnly": true
7
+ },
8
+ "include": ["src/**/*.ts"],
9
+ "exclude": [
10
+ "node_modules",
11
+ "dist",
12
+ "coverage",
13
+ "vitest.config.ts",
14
+ "vitest.setup.ts",
15
+ "vitest.shims.d.ts",
16
+ ".storybook",
17
+ "**/*.test.ts",
18
+ "src/**/*.stories.ts"
19
+ ]
20
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,55 @@
1
+ {
2
+ // Visit https://aka.ms/tsconfig to read more about this file
3
+ "compilerOptions": {
4
+ // File Layout
5
+ "rootDir": ".",
6
+ "outDir": "./dist",
7
+
8
+ // Environment Settings
9
+ // See also https://aka.ms/tsconfig/module
10
+ "module": "nodenext",
11
+ "target": "esnext",
12
+ "types": [],
13
+ // For nodejs:
14
+ // "lib": ["esnext"],
15
+ // "types": ["node"],
16
+ // and npm install -D @types/node
17
+
18
+ // Other Outputs
19
+ "sourceMap": true,
20
+ "declaration": true,
21
+ "declarationMap": true,
22
+ "emitDeclarationOnly": true,
23
+
24
+ // Stricter Typechecking Options
25
+ "noUncheckedIndexedAccess": true,
26
+ "exactOptionalPropertyTypes": true,
27
+ "allowImportingTsExtensions": true,
28
+
29
+ // Style Options
30
+ // "noImplicitReturns": true,
31
+ // "noImplicitOverride": true,
32
+ // "noUnusedLocals": true,
33
+ // "noUnusedParameters": true,
34
+ // "noFallthroughCasesInSwitch": true,
35
+ // "noPropertyAccessFromIndexSignature": true,
36
+
37
+ // Recommended Options
38
+ "strict": true,
39
+ // "jsx": "react-jsx",
40
+ "verbatimModuleSyntax": true,
41
+ "isolatedModules": true,
42
+ "noUncheckedSideEffectImports": true,
43
+ "moduleDetection": "force",
44
+ "skipLibCheck": true
45
+ },
46
+ "exclude": [
47
+ "node_modules",
48
+ "dist",
49
+ "coverage",
50
+ "vitest.config.ts",
51
+ "vitest.setup.ts",
52
+ "vitest.shims.d.ts",
53
+ ".storybook"
54
+ ]
55
+ }
@@ -0,0 +1,34 @@
1
+ import path from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { defineConfig } from 'vitest/config';
4
+
5
+ const dirname =
6
+ typeof __dirname !== 'undefined'
7
+ ? __dirname
8
+ : path.dirname(fileURLToPath(import.meta.url));
9
+
10
+ export default defineConfig({
11
+ resolve: {
12
+ alias: {
13
+ '@': path.resolve(dirname, './src'),
14
+ },
15
+ },
16
+ test: {
17
+ environment: 'jsdom',
18
+ globals: true,
19
+ setupFiles: ['./vitest.setup.ts'],
20
+ include: ['src/**/*.test.ts'],
21
+ browser: {
22
+ enabled: false,
23
+ },
24
+ coverage: {
25
+ provider: 'v8',
26
+ thresholds: {
27
+ lines: 90,
28
+ functions: 90,
29
+ branches: 90,
30
+ statements: 90,
31
+ },
32
+ }
33
+ },
34
+ });
@@ -0,0 +1,3 @@
1
+ import { GlobalRegistrator } from '@happy-dom/global-registrator';
2
+
3
+ GlobalRegistrator.register();
@@ -0,0 +1 @@
1
+ /// <reference types="@vitest/browser-playwright" />