@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
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -0,0 +1,89 @@
1
+ import prettier from 'eslint-config-prettier';
2
+ import sonarjs from 'eslint-plugin-sonarjs';
3
+ import storybook from 'eslint-plugin-storybook';
4
+ import wc from 'eslint-plugin-wc';
5
+ import globals from 'globals';
6
+ import tsEslint from 'typescript-eslint';
7
+
8
+ export default tsEslint.config(
9
+ {
10
+ ignores: ['dist/**', 'node_modules/**', 'coverage/**', '.storybook/**'],
11
+ },
12
+
13
+ ...tsEslint.configs.recommended,
14
+ sonarjs.configs.recommended,
15
+
16
+ {
17
+ files: ['src/**/*.ts'],
18
+ plugins: {
19
+ wc,
20
+ },
21
+ languageOptions: {
22
+ parser: tsEslint.parser,
23
+ parserOptions: {
24
+ projectService: true,
25
+ tsconfigRootDir: import.meta.dirname,
26
+ },
27
+ globals: {
28
+ ...globals.browser,
29
+ ...globals.node,
30
+ },
31
+ },
32
+ rules: {
33
+ '@typescript-eslint/explicit-member-accessibility': [
34
+ 'error',
35
+ {
36
+ accessibility: 'explicit',
37
+ overrides: {
38
+ constructors: 'no-public',
39
+ },
40
+ },
41
+ ],
42
+ '@typescript-eslint/consistent-type-exports': 'error',
43
+ '@typescript-eslint/no-import-type-side-effects': 'error',
44
+ '@typescript-eslint/no-explicit-any': 'error',
45
+
46
+ ...wc.configs.recommended.rules,
47
+
48
+ 'wc/no-constructor-attributes': 'error',
49
+ 'wc/no-invalid-element-name': 'error',
50
+ 'wc/guard-super-call': 'error',
51
+ 'wc/attach-shadow-constructor': 'off',
52
+ 'wc/no-self-class': 'off',
53
+
54
+ 'sonarjs/todo-tag': 'warn',
55
+ 'sonarjs/no-selector-parameter': 'off',
56
+ },
57
+ },
58
+
59
+ {
60
+ files: ['**/*.test.ts', '**/*.spec.ts', 'src/**/*.stories.ts'],
61
+ languageOptions: {
62
+ parserOptions: {
63
+ projectService: {
64
+ allowDefaultProject: ['*.ts', '*.tsx'],
65
+ extendedReports: true,
66
+ },
67
+ tsconfigRootDir: import.meta.dirname,
68
+ },
69
+ globals: {
70
+ ...globals.node,
71
+ Bun: 'readonly',
72
+ describe: 'readonly',
73
+ it: 'readonly',
74
+ expect: 'readonly',
75
+ test: 'readonly',
76
+ beforeAll: 'readonly',
77
+ afterAll: 'readonly',
78
+ beforeEach: 'readonly',
79
+ afterEach: 'readonly',
80
+ },
81
+ },
82
+ rules: {
83
+ '@typescript-eslint/no-unused-expressions': 'error',
84
+ },
85
+ },
86
+
87
+ ...storybook.configs['flat/recommended'],
88
+ prettier,
89
+ );
package/index.ts ADDED
@@ -0,0 +1,15 @@
1
+ export { Router } from './src/router/Router.ts';
2
+ export { RouterElement } from './src/elements/RouterElement.ts';
3
+ export { RouteElement } from './src/elements/RouteElement.ts';
4
+
5
+ export type {
6
+ RouteComponent,
7
+ RouteContext,
8
+ RouteDefinition,
9
+ RouteGuard,
10
+ RouteParams,
11
+ CompiledRoute,
12
+ MatchedRoute,
13
+ } from './src/models/index.ts';
14
+
15
+ export { compileRoute, matchRoute } from './src/utils/index.ts';
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@wavelengthusaf/client-router",
3
+ "version": "0.0.1",
4
+ "description": "Native TypeScript Router",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "scripts": {
8
+ "build": "tsc -p tsconfig.build.json",
9
+ "lint": "eslint .",
10
+ "lint:fix": "eslint . --fix",
11
+ "prettier": "prettier --write .",
12
+ "type-check": "tsc --noEmit",
13
+ "test": "vitest run --coverage",
14
+ "storybook": "storybook dev -p 6006",
15
+ "build-storybook": "storybook build",
16
+ "test-storybook": "test-storybook",
17
+ "prepare": "husky"
18
+ },
19
+ "keywords": [
20
+ "router"
21
+ ],
22
+ "author": "Wavelength",
23
+ "license": "ISC",
24
+ "type": "module",
25
+ "devDependencies": {
26
+ "@happy-dom/global-registrator": "^20.9.0",
27
+ "@storybook/addon-a11y": "^10.3.5",
28
+ "@storybook/addon-docs": "^10.3.5",
29
+ "@storybook/addon-links": "^10.3.5",
30
+ "@storybook/addon-vitest": "^10.3.5",
31
+ "@storybook/testing-library": "^0.2.1",
32
+ "@storybook/web-components": "^10.3.5",
33
+ "@storybook/web-components-vite": "^10.3.5",
34
+ "@trivago/prettier-plugin-sort-imports": "^6.0.2",
35
+ "@types/jsdom": "^28.0.1",
36
+ "@types/node": "^25.6.0",
37
+ "@vitest/coverage-v8": "^4.1.4",
38
+ "eslint": "^10.2.0",
39
+ "eslint-config-prettier": "^10.1.8",
40
+ "eslint-plugin-sonarjs": "^4.0.2",
41
+ "eslint-plugin-storybook": "^10.3.5",
42
+ "eslint-plugin-wc": "^3.1.0",
43
+ "globals": "^17.5.0",
44
+ "husky": "^9.1.7",
45
+ "jsdom": "^29.0.2",
46
+ "opencode-ai": "^1.4.6",
47
+ "playwright": "^1.59.1",
48
+ "prettier": "^3.8.2",
49
+ "storybook": "^10.3.5",
50
+ "typescript": "^6.0.2",
51
+ "typescript-eslint": "^8.58.2",
52
+ "vitest": "^4.1.4"
53
+ }
54
+ }
@@ -0,0 +1,30 @@
1
+ import type { Config } from 'prettier';
2
+
3
+ const config: Config = {
4
+ endOfLine: 'lf',
5
+ arrowParens: 'always',
6
+ semi: true,
7
+ trailingComma: 'all',
8
+ singleQuote: true,
9
+ printWidth: 80,
10
+ tabWidth: 2,
11
+ useTabs: false,
12
+ plugins: ['@trivago/prettier-plugin-sort-imports'],
13
+ importOrder: [
14
+ '^bun.*$',
15
+ '<THIRD_PARTY_MODULES>',
16
+ '^@/(.*)$',
17
+ '^[../]',
18
+ '^[./]',
19
+ '(.css|.scss)$',
20
+ ],
21
+ importOrderSeparation: true,
22
+ importOrderSortSpecifiers: true,
23
+ importOrderParserPlugins: [
24
+ 'typescript',
25
+ 'decorators-legacy',
26
+ 'importAssertions',
27
+ ],
28
+ };
29
+
30
+ export default config;
@@ -0,0 +1,67 @@
1
+ import type {
2
+ RouteComponent,
3
+ RouteContext,
4
+ RouteDefinition,
5
+ } from '../models/index.ts';
6
+
7
+ export interface RouteElementEnv {
8
+ document?: Document;
9
+ }
10
+
11
+ export class RouteElement extends HTMLElement {
12
+ public static readonly tag = 'wavelength-route';
13
+
14
+ public get path(): string {
15
+ return this.getAttribute('path') ?? '*';
16
+ }
17
+
18
+ public get componentTag(): string {
19
+ return this.getAttribute('component') ?? '';
20
+ }
21
+
22
+ public toDefinition(env: RouteElementEnv = {}): RouteDefinition | null {
23
+ const tag: string = this.componentTag;
24
+ const doc: Document | undefined = env.document;
25
+
26
+ if (!tag) {
27
+ console.warn(
28
+ `[route] Missing 'component' attribute on path="${this.path}"`,
29
+ );
30
+ return null;
31
+ }
32
+
33
+ if (!doc) {
34
+ return {
35
+ path: this.path,
36
+ routeComponent: {
37
+ mount() {},
38
+ unmount() {},
39
+ },
40
+ };
41
+ }
42
+
43
+ const routeComponent: RouteComponent = {
44
+ mount: (container: HTMLElement, routeContext: RouteContext) => {
45
+ const element = doc.createElement(tag) as HTMLElement & {
46
+ routeContext?: RouteContext;
47
+ };
48
+
49
+ element.routeContext = routeContext;
50
+
51
+ container.appendChild(element);
52
+ },
53
+ unmount: () => {
54
+ // optional: future cleanup hook
55
+ },
56
+ };
57
+
58
+ return {
59
+ path: this.path,
60
+ routeComponent,
61
+ };
62
+ }
63
+ }
64
+
65
+ if (!customElements.get(RouteElement.tag)) {
66
+ customElements.define(RouteElement.tag, RouteElement);
67
+ }
@@ -0,0 +1,97 @@
1
+ import type { RouteDefinition, RouteGuard } from '../models/index.ts';
2
+ import { Router } from '../router/Router.ts';
3
+ import type { RouteElement } from './RouteElement.ts';
4
+
5
+ export class RouterElement extends HTMLElement {
6
+ public static readonly tag = 'wavelength-router';
7
+
8
+ #router?: Router;
9
+
10
+ public static get observedAttributes(): string[] {
11
+ return ['hash-mode', 'guard'];
12
+ }
13
+
14
+ public connectedCallback(): void {
15
+ this.#bootstrap();
16
+ }
17
+
18
+ public disconnectedCallback(): void {
19
+ this.#teardown();
20
+ }
21
+
22
+ public attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void {
23
+ if (!this.isConnected || oldValue === newValue) return;
24
+ this.#bootstrap();
25
+ }
26
+
27
+ public navigate(path: string): void {
28
+ this.#router?.navigate(path);
29
+ }
30
+
31
+ public replace(path: string): void {
32
+ this.#router?.replace(path);
33
+ }
34
+
35
+ public back(): void {
36
+ this.#router?.back();
37
+ }
38
+
39
+ #bootstrap(): void {
40
+ this.#teardown();
41
+
42
+ const routes: RouteDefinition[] = this.#collectRoutes();
43
+
44
+ if (routes.length === 0) {
45
+ console.warn('[wavelength-router] No <wavelength-route> children found.');
46
+ return;
47
+ }
48
+
49
+ const guard: RouteGuard | undefined =this.#resolveGuard();
50
+ this.#router = new Router(
51
+ routes,
52
+ {
53
+ outlet: this,
54
+ hashMode: this.hasAttribute('hash-mode'),
55
+ ...(guard ? { beforeEach: guard } : {}),
56
+ },
57
+ {
58
+ window: window,
59
+ document: document,
60
+ },
61
+ );
62
+
63
+ this.#router.start();
64
+ }
65
+
66
+ #teardown(): void {
67
+ this.#router?.destroy();
68
+ }
69
+
70
+ #collectRoutes(): RouteDefinition[] {
71
+ const routeElements: RouteElement[] = Array.from(
72
+ this.querySelectorAll<RouteElement>('wavelength-route'),
73
+ );
74
+
75
+ return routeElements
76
+ .map((routeElement: RouteElement) => routeElement.toDefinition({ document }))
77
+ .filter((routeDefinition: RouteDefinition | null): routeDefinition is RouteDefinition => routeDefinition !== null);
78
+ }
79
+
80
+ #resolveGuard(): RouteGuard | undefined {
81
+ const guardName: string | null = this.getAttribute('guard');
82
+ if (!guardName) return undefined;
83
+
84
+ const fn: unknown = (window as unknown as Record<string, unknown>)[guardName];
85
+
86
+ if (typeof fn !== 'function') {
87
+ console.warn(`[wavelength-router] Guard "${guardName}" not found on window.`);
88
+ return undefined;
89
+ }
90
+
91
+ return fn as RouteGuard;
92
+ }
93
+ }
94
+
95
+ if (!customElements.get(RouterElement.tag)) {
96
+ customElements.define(RouterElement.tag, RouterElement);
97
+ }