@ydant/router 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 cwd-k2
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,157 @@
1
+ # @ydant/router
2
+
3
+ SPA routing for Ydant using History API.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pnpm add @ydant/router
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```typescript
14
+ import { div, nav, text, type Component } from "@ydant/core";
15
+ import { mount } from "@ydant/dom";
16
+ import { RouterView, RouterLink, useRoute, navigate } from "@ydant/router";
17
+
18
+ // Define pages
19
+ const HomePage: Component = () => div(() => [text("Home Page")]);
20
+
21
+ const UserPage: Component = () =>
22
+ div(function* () {
23
+ const route = useRoute();
24
+ yield* text(`User ID: ${route.params.id}`);
25
+ });
26
+
27
+ const NotFoundPage: Component = () => div(() => [text("404 - Not Found")]);
28
+
29
+ // Main app with routing
30
+ const App: Component = () =>
31
+ div(function* () {
32
+ // Navigation
33
+ yield* nav(() => [
34
+ RouterLink({ href: "/", children: () => text("Home") }),
35
+ RouterLink({ href: "/users/1", children: () => text("User 1") }),
36
+ ]);
37
+
38
+ // Route definitions
39
+ yield* RouterView({
40
+ routes: [
41
+ { path: "/", component: HomePage },
42
+ { path: "/users/:id", component: UserPage },
43
+ { path: "*", component: NotFoundPage },
44
+ ],
45
+ });
46
+ });
47
+
48
+ mount(App, document.getElementById("app")!);
49
+ ```
50
+
51
+ ## API
52
+
53
+ ### RouterView
54
+
55
+ ```typescript
56
+ function RouterView(props: RouterViewProps): ElementGenerator;
57
+
58
+ interface RouterViewProps {
59
+ routes: RouteDefinition[];
60
+ base?: string;
61
+ }
62
+
63
+ interface RouteDefinition {
64
+ path: string;
65
+ component: Component;
66
+ guard?: () => boolean | Promise<boolean>;
67
+ }
68
+ ```
69
+
70
+ Renders the component matching the current path.
71
+
72
+ #### Route Guards
73
+
74
+ Route guards control access to routes. They can be synchronous or asynchronous:
75
+
76
+ ```typescript
77
+ // Synchronous guard
78
+ {
79
+ path: "/admin",
80
+ component: AdminPage,
81
+ guard: () => isAuthenticated(),
82
+ }
83
+
84
+ // Async guard (e.g., checking server-side permissions)
85
+ {
86
+ path: "/settings",
87
+ component: SettingsPage,
88
+ guard: async () => {
89
+ const response = await fetch("/api/permissions");
90
+ const { canAccess } = await response.json();
91
+ return canAccess;
92
+ },
93
+ }
94
+ ```
95
+
96
+ When a guard returns or resolves to `false`, the route is blocked and an empty view is rendered.
97
+
98
+ ### RouterLink
99
+
100
+ ```typescript
101
+ function RouterLink(props: RouterLinkProps): ElementGenerator;
102
+
103
+ interface RouterLinkProps {
104
+ href: string;
105
+ children: () => Render;
106
+ activeClass?: string;
107
+ }
108
+ ```
109
+
110
+ Creates a navigation link that updates the URL without page reload.
111
+
112
+ ### useRoute
113
+
114
+ ```typescript
115
+ function useRoute(): RouteInfo;
116
+
117
+ interface RouteInfo {
118
+ path: string;
119
+ params: Record<string, string>;
120
+ query: Record<string, string>;
121
+ hash: string;
122
+ }
123
+ ```
124
+
125
+ Returns current route information including path parameters.
126
+
127
+ ### navigate
128
+
129
+ ```typescript
130
+ function navigate(path: string, replace?: boolean): void;
131
+ ```
132
+
133
+ Programmatically navigate to a path. If `replace` is true, replaces the current history entry.
134
+
135
+ ### goBack / goForward
136
+
137
+ ```typescript
138
+ function goBack(): void;
139
+ function goForward(): void;
140
+ ```
141
+
142
+ Navigate through browser history.
143
+
144
+ ## Path Patterns
145
+
146
+ - `/users` - Exact match
147
+ - `/users/:id` - Path parameter (captured as `params.id`)
148
+ - `/users/*` - Wildcard (matches any suffix)
149
+ - `*` - Catch-all (404 page)
150
+
151
+ ## Module Structure
152
+
153
+ - `types.ts` - Type definitions
154
+ - `matching.ts` - Path matching utilities
155
+ - `state.ts` - Route state management
156
+ - `navigation.ts` - Navigation functions
157
+ - `components.ts` - RouterView, RouterLink
@@ -0,0 +1,15 @@
1
+ import { Render } from '@ydant/core';
2
+ import { RouterLinkProps } from './types';
3
+ /**
4
+ * RouterLink コンポーネント
5
+ *
6
+ * クリック時にブラウザのデフォルト動作を防ぎ、
7
+ * History API を使用して SPA ルーティングを行う。
8
+ *
9
+ * @param props - RouterLink のプロパティ
10
+ * @param props.href - リンク先のパス
11
+ * @param props.children - リンクの子要素
12
+ * @param props.activeClass - アクティブ時に適用するクラス名(オプション)
13
+ * @returns リンク要素の Render
14
+ */
15
+ export declare function RouterLink(props: RouterLinkProps): Render;
@@ -0,0 +1,14 @@
1
+ import { Render } from '@ydant/core';
2
+ import { RouterViewProps } from './types';
3
+ /**
4
+ * RouterView コンポーネント
5
+ *
6
+ * 現在のパスに基づいて適切なコンポーネントを表示する。
7
+ * History API の popstate イベントを監視し、URL 変更時に再レンダリングする。
8
+ *
9
+ * @param props - RouterView のプロパティ
10
+ * @param props.routes - ルート定義の配列
11
+ * @param props.base - ベースパス(オプション、デフォルト: "")
12
+ * @returns コンテナ要素の Render
13
+ */
14
+ export declare function RouterView(props: RouterViewProps): Render;
@@ -0,0 +1,4 @@
1
+ export type { RouteDefinition, RouteInfo, RouterViewProps, RouterLinkProps } from './types';
2
+ export { getRoute, navigate, goBack, goForward } from './navigation';
3
+ export { RouterView } from './RouterView';
4
+ export { RouterLink } from './RouterLink';
@@ -0,0 +1,123 @@
1
+ import { div as f, onMount as m, a as g, attr as l, on as y } from "@ydant/base";
2
+ function R(t) {
3
+ const e = t.match(/:([^/]+)/g);
4
+ return e ? e.map((o) => o.slice(1)) : [];
5
+ }
6
+ function v(t) {
7
+ if (t === "*")
8
+ return /.*/;
9
+ const e = "___PARAM___", r = t.replace(/:([^/]+)/g, e).replace(/[.*+?^${}()|[\]\\]/g, "\\$&").replace(new RegExp(e, "g"), "([^/]+)");
10
+ return new RegExp(`^${r}$`);
11
+ }
12
+ function h(t) {
13
+ const e = {};
14
+ if (t.startsWith("?") && (t = t.slice(1)), t)
15
+ for (const o of t.split("&")) {
16
+ const [n, r] = o.split("=");
17
+ n && (e[decodeURIComponent(n)] = decodeURIComponent(r || ""));
18
+ }
19
+ return e;
20
+ }
21
+ function P(t, e) {
22
+ const o = v(e), n = t.match(o);
23
+ if (!n)
24
+ return { match: !1, params: {} };
25
+ const r = R(e), a = {};
26
+ for (let i = 0; i < r.length; i++)
27
+ a[r[i]] = n[i + 1] || "";
28
+ return { match: !0, params: a };
29
+ }
30
+ function x() {
31
+ return {
32
+ path: typeof window < "u" ? window.location.pathname : "/",
33
+ params: {},
34
+ query: h(typeof window < "u" ? window.location.search : ""),
35
+ hash: typeof window < "u" ? window.location.hash : ""
36
+ };
37
+ }
38
+ let c = x();
39
+ const d = /* @__PURE__ */ new Set();
40
+ function p(t) {
41
+ const e = new URL(t, window.location.origin);
42
+ c = {
43
+ path: e.pathname,
44
+ params: {},
45
+ query: h(e.search),
46
+ hash: e.hash
47
+ };
48
+ for (const o of d)
49
+ o();
50
+ }
51
+ function k() {
52
+ return c;
53
+ }
54
+ function S(t, e = !1) {
55
+ e ? window.history.replaceState(null, "", t) : window.history.pushState(null, "", t), p(t);
56
+ }
57
+ function C() {
58
+ window.history.back();
59
+ }
60
+ function E() {
61
+ window.history.forward();
62
+ }
63
+ function w(t, e) {
64
+ const o = c.path.startsWith(e) ? c.path.slice(e.length) || "/" : c.path;
65
+ for (const n of t) {
66
+ const { match: r, params: a } = P(o, n.path);
67
+ if (r)
68
+ return { route: n, params: a };
69
+ }
70
+ return null;
71
+ }
72
+ function s(t, e) {
73
+ const o = w(t, e);
74
+ if (!o) return [];
75
+ const { route: n, params: r } = o;
76
+ if (c.params = r, !n.guard)
77
+ return [n.component()];
78
+ const a = n.guard();
79
+ return a instanceof Promise ? [] : a ? [n.component()] : [];
80
+ }
81
+ async function u(t, e, o) {
82
+ const n = w(t, e);
83
+ if (!n) return;
84
+ const { route: r, params: a } = n;
85
+ if (r.guard) {
86
+ const i = r.guard();
87
+ i instanceof Promise && await i && (c.params = a, o(() => [r.component()]));
88
+ }
89
+ }
90
+ function M(t) {
91
+ const { routes: e, base: o = "" } = t;
92
+ return f(function* () {
93
+ const n = yield* f(() => s(e, o));
94
+ u(e, o, (r) => n.refresh(r)), yield* m(() => {
95
+ const r = () => {
96
+ p(window.location.pathname), n.refresh(() => s(e, o)), u(e, o, (i) => n.refresh(i));
97
+ };
98
+ window.addEventListener("popstate", r);
99
+ const a = () => {
100
+ n.refresh(() => s(e, o)), u(e, o, (i) => n.refresh(i));
101
+ };
102
+ return d.add(a), () => {
103
+ window.removeEventListener("popstate", r), d.delete(a);
104
+ };
105
+ });
106
+ });
107
+ }
108
+ function $(t) {
109
+ const { href: e, children: o, activeClass: n } = t;
110
+ return g(function* () {
111
+ yield* l("href", e), n && c.path === e && (yield* l("class", n)), yield* y("click", (r) => {
112
+ r.preventDefault(), S(e);
113
+ }), yield* o();
114
+ });
115
+ }
116
+ export {
117
+ $ as RouterLink,
118
+ M as RouterView,
119
+ k as getRoute,
120
+ C as goBack,
121
+ E as goForward,
122
+ S as navigate
123
+ };
@@ -0,0 +1 @@
1
+ (function(i,c){typeof exports=="object"&&typeof module<"u"?c(exports,require("@ydant/base")):typeof define=="function"&&define.amd?define(["exports","@ydant/base"],c):(i=typeof globalThis<"u"?globalThis:i||self,c(i.YdantRouter={},i.YdantBase))})(this,(function(i,c){"use strict";function g(t){const e=t.match(/:([^/]+)/g);return e?e.map(o=>o.slice(1)):[]}function y(t){if(t==="*")return/.*/;const e="___PARAM___",r=t.replace(/:([^/]+)/g,e).replace(/[.*+?^${}()|[\]\\]/g,"\\$&").replace(new RegExp(e,"g"),"([^/]+)");return new RegExp(`^${r}$`)}function h(t){const e={};if(t.startsWith("?")&&(t=t.slice(1)),t)for(const o of t.split("&")){const[n,r]=o.split("=");n&&(e[decodeURIComponent(n)]=decodeURIComponent(r||""))}return e}function R(t,e){const o=y(e),n=t.match(o);if(!n)return{match:!1,params:{}};const r=g(e),a={};for(let s=0;s<r.length;s++)a[r[s]]=n[s+1]||"";return{match:!0,params:a}}function v(){return{path:typeof window<"u"?window.location.pathname:"/",params:{},query:h(typeof window<"u"?window.location.search:""),hash:typeof window<"u"?window.location.hash:""}}let u=v();const d=new Set;function p(t){const e=new URL(t,window.location.origin);u={path:e.pathname,params:{},query:h(e.search),hash:e.hash};for(const o of d)o()}function P(){return u}function w(t,e=!1){e?window.history.replaceState(null,"",t):window.history.pushState(null,"",t),p(t)}function S(){window.history.back()}function k(){window.history.forward()}function m(t,e){const o=u.path.startsWith(e)?u.path.slice(e.length)||"/":u.path;for(const n of t){const{match:r,params:a}=R(o,n.path);if(r)return{route:n,params:a}}return null}function f(t,e){const o=m(t,e);if(!o)return[];const{route:n,params:r}=o;if(u.params=r,!n.guard)return[n.component()];const a=n.guard();return a instanceof Promise?[]:a?[n.component()]:[]}async function l(t,e,o){const n=m(t,e);if(!n)return;const{route:r,params:a}=n;if(r.guard){const s=r.guard();s instanceof Promise&&await s&&(u.params=a,o(()=>[r.component()]))}}function L(t){const{routes:e,base:o=""}=t;return c.div(function*(){const n=yield*c.div(()=>f(e,o));l(e,o,r=>n.refresh(r)),yield*c.onMount(()=>{const r=()=>{p(window.location.pathname),n.refresh(()=>f(e,o)),l(e,o,s=>n.refresh(s))};window.addEventListener("popstate",r);const a=()=>{n.refresh(()=>f(e,o)),l(e,o,s=>n.refresh(s))};return d.add(a),()=>{window.removeEventListener("popstate",r),d.delete(a)}})})}function _(t){const{href:e,children:o,activeClass:n}=t;return c.a(function*(){yield*c.attr("href",e),n&&u.path===e&&(yield*c.attr("class",n)),yield*c.on("click",r=>{r.preventDefault(),w(e)}),yield*o()})}i.RouterLink=_,i.RouterView=L,i.getRoute=P,i.goBack=S,i.goForward=k,i.navigate=w,Object.defineProperty(i,Symbol.toStringTag,{value:"Module"})}));
@@ -0,0 +1,14 @@
1
+ /**
2
+ * パスマッチングユーティリティ
3
+ */
4
+ /** パスパターンからパラメータ名を抽出 */
5
+ export declare function extractParamNames(pattern: string): string[];
6
+ /** パスパターンを正規表現に変換 */
7
+ export declare function patternToRegex(pattern: string): RegExp;
8
+ /** クエリ文字列をパース */
9
+ export declare function parseQuery(search: string): Record<string, string>;
10
+ /** パスがパターンにマッチするか確認し、パラメータを抽出 */
11
+ export declare function matchPath(path: string, pattern: string): {
12
+ match: boolean;
13
+ params: Record<string, string>;
14
+ };
@@ -0,0 +1,22 @@
1
+ import { RouteInfo } from './types';
2
+ /**
3
+ * 現在のルート情報を取得
4
+ *
5
+ * @returns 現在のルート情報(パス、パラメータ、クエリ)
6
+ */
7
+ export declare function getRoute(): RouteInfo;
8
+ /**
9
+ * プログラムによるナビゲーション
10
+ *
11
+ * @param path - 遷移先のパス
12
+ * @param replace - true の場合、履歴に追加せずに置き換え
13
+ */
14
+ export declare function navigate(path: string, replace?: boolean): void;
15
+ /**
16
+ * 履歴を戻る
17
+ */
18
+ export declare function goBack(): void;
19
+ /**
20
+ * 履歴を進む
21
+ */
22
+ export declare function goForward(): void;
@@ -0,0 +1,12 @@
1
+ import { RouteInfo } from './types';
2
+ /** 現在のルート情報 */
3
+ export declare let currentRoute: RouteInfo;
4
+ /** ルート変更リスナー */
5
+ export declare const routeListeners: Set<() => void>;
6
+ /**
7
+ * テスト用: 状態をリセット
8
+ * @internal
9
+ */
10
+ export declare function __resetForTesting__(): void;
11
+ /** ルート情報を更新 */
12
+ export declare function updateRoute(path: string): void;
@@ -0,0 +1,37 @@
1
+ import { Component, Instruction } from '@ydant/core';
2
+ /** ルート定義 */
3
+ export interface RouteDefinition {
4
+ /** パスパターン(例: "/users/:id") */
5
+ path: string;
6
+ /** パスにマッチした時に表示するコンポーネント */
7
+ component: Component;
8
+ /** ルートガード(false を返すとナビゲーションをキャンセル) */
9
+ guard?: () => boolean | Promise<boolean>;
10
+ }
11
+ /** ルート情報 */
12
+ export interface RouteInfo {
13
+ /** 現在のパス */
14
+ path: string;
15
+ /** パスパラメータ(例: { id: "123" }) */
16
+ params: Record<string, string>;
17
+ /** クエリパラメータ */
18
+ query: Record<string, string>;
19
+ /** ハッシュ */
20
+ hash: string;
21
+ }
22
+ /** RouterView コンポーネントの props */
23
+ export interface RouterViewProps {
24
+ /** ルート定義の配列 */
25
+ routes: RouteDefinition[];
26
+ /** ベースパス(オプション) */
27
+ base?: string;
28
+ }
29
+ /** RouterLink コンポーネントの props */
30
+ export interface RouterLinkProps {
31
+ /** リンク先のパス */
32
+ href: string;
33
+ /** リンクの子要素 */
34
+ children: () => Instruction;
35
+ /** アクティブ時に追加するクラス */
36
+ activeClass?: string;
37
+ }
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@ydant/router",
3
+ "version": "0.1.0",
4
+ "description": "Router for Ydant SPA applications",
5
+ "keywords": [
6
+ "navigation",
7
+ "router",
8
+ "spa",
9
+ "ydant"
10
+ ],
11
+ "homepage": "https://github.com/cwd-k2/ydant#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/cwd-k2/ydant/issues"
14
+ },
15
+ "license": "MIT",
16
+ "author": "cwd-k2",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "https://github.com/cwd-k2/ydant.git",
20
+ "directory": "packages/router"
21
+ },
22
+ "files": [
23
+ "dist",
24
+ "LICENSE",
25
+ "README.md"
26
+ ],
27
+ "main": "./dist/index.umd.js",
28
+ "module": "./dist/index.es.js",
29
+ "types": "./dist/index.d.ts",
30
+ "exports": {
31
+ ".": {
32
+ "types": "./dist/index.d.ts",
33
+ "@ydant/dev": {
34
+ "types": "./src/index.ts",
35
+ "default": "./src/index.ts"
36
+ },
37
+ "import": "./dist/index.es.js",
38
+ "require": "./dist/index.umd.js"
39
+ }
40
+ },
41
+ "peerDependencies": {
42
+ "@ydant/base": "0.1.0",
43
+ "@ydant/core": "0.1.0"
44
+ },
45
+ "scripts": {
46
+ "build": "vite build",
47
+ "typecheck": "tsc --noEmit"
48
+ }
49
+ }