@mandujs/core 0.5.7 โ 0.7.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/package.json +1 -1
- package/src/bundler/build.ts +226 -0
- package/src/bundler/types.ts +2 -0
- package/src/client/Link.tsx +209 -0
- package/src/client/hooks.ts +267 -0
- package/src/client/index.ts +90 -2
- package/src/client/router.ts +387 -0
- package/src/client/serialize.ts +404 -0
- package/src/filling/filling.ts +96 -0
- package/src/runtime/compose.ts +222 -0
- package/src/runtime/index.ts +2 -0
- package/src/runtime/lifecycle.ts +360 -0
- package/src/runtime/server.ts +18 -0
- package/src/runtime/ssr.ts +65 -0
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mandu Router Hooks ๐ช
|
|
3
|
+
* React hooks for client-side routing
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { useState, useEffect, useCallback, useSyncExternalStore } from "react";
|
|
7
|
+
import {
|
|
8
|
+
subscribe,
|
|
9
|
+
getRouterState,
|
|
10
|
+
getCurrentRoute,
|
|
11
|
+
getLoaderData,
|
|
12
|
+
getNavigationState,
|
|
13
|
+
navigate,
|
|
14
|
+
type RouteInfo,
|
|
15
|
+
type NavigationState,
|
|
16
|
+
type NavigateOptions,
|
|
17
|
+
} from "./router";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* ๋ผ์ฐํฐ ์ํ ์ ์ฒด ์ ๊ทผ
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```tsx
|
|
24
|
+
* const { currentRoute, loaderData, navigation } = useRouterState();
|
|
25
|
+
* ```
|
|
26
|
+
*/
|
|
27
|
+
export function useRouterState() {
|
|
28
|
+
return useSyncExternalStore(
|
|
29
|
+
subscribe,
|
|
30
|
+
getRouterState,
|
|
31
|
+
getRouterState // SSR์์๋ ๋์ผ
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* ํ์ฌ ๋ผ์ฐํธ ์ ๋ณด
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```tsx
|
|
40
|
+
* const route = useRoute();
|
|
41
|
+
* console.log(route?.id, route?.params);
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export function useRoute(): RouteInfo | null {
|
|
45
|
+
const state = useRouterState();
|
|
46
|
+
return state.currentRoute;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* URL ํ๋ผ๋ฏธํฐ ์ ๊ทผ
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```tsx
|
|
54
|
+
* // URL: /users/123
|
|
55
|
+
* const { id } = useParams<{ id: string }>();
|
|
56
|
+
* console.log(id); // "123"
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
export function useParams<T extends Record<string, string> = Record<string, string>>(): T {
|
|
60
|
+
const route = useRoute();
|
|
61
|
+
return (route?.params ?? {}) as T;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* ํ์ฌ ๊ฒฝ๋ก๋ช
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* ```tsx
|
|
69
|
+
* const pathname = usePathname();
|
|
70
|
+
* console.log(pathname); // "/users/123"
|
|
71
|
+
* ```
|
|
72
|
+
*/
|
|
73
|
+
export function usePathname(): string {
|
|
74
|
+
const [pathname, setPathname] = useState(() =>
|
|
75
|
+
typeof window !== "undefined" ? window.location.pathname : "/"
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
const handleChange = () => {
|
|
80
|
+
setPathname(window.location.pathname);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
window.addEventListener("popstate", handleChange);
|
|
84
|
+
|
|
85
|
+
// ๋ผ์ฐํฐ ์ํ ๋ณ๊ฒฝ ๊ตฌ๋
|
|
86
|
+
const unsubscribe = subscribe(() => {
|
|
87
|
+
setPathname(window.location.pathname);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
return () => {
|
|
91
|
+
window.removeEventListener("popstate", handleChange);
|
|
92
|
+
unsubscribe();
|
|
93
|
+
};
|
|
94
|
+
}, []);
|
|
95
|
+
|
|
96
|
+
return pathname;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* ํ์ฌ ๊ฒ์ ํ๋ผ๋ฏธํฐ (์ฟผ๋ฆฌ ์คํธ๋ง)
|
|
101
|
+
*
|
|
102
|
+
* @example
|
|
103
|
+
* ```tsx
|
|
104
|
+
* // URL: /search?q=hello&page=2
|
|
105
|
+
* const searchParams = useSearchParams();
|
|
106
|
+
* console.log(searchParams.get("q")); // "hello"
|
|
107
|
+
* ```
|
|
108
|
+
*/
|
|
109
|
+
export function useSearchParams(): URLSearchParams {
|
|
110
|
+
const [searchParams, setSearchParams] = useState(() =>
|
|
111
|
+
typeof window !== "undefined"
|
|
112
|
+
? new URLSearchParams(window.location.search)
|
|
113
|
+
: new URLSearchParams()
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
useEffect(() => {
|
|
117
|
+
const handleChange = () => {
|
|
118
|
+
setSearchParams(new URLSearchParams(window.location.search));
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
window.addEventListener("popstate", handleChange);
|
|
122
|
+
|
|
123
|
+
const unsubscribe = subscribe(() => {
|
|
124
|
+
setSearchParams(new URLSearchParams(window.location.search));
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
return () => {
|
|
128
|
+
window.removeEventListener("popstate", handleChange);
|
|
129
|
+
unsubscribe();
|
|
130
|
+
};
|
|
131
|
+
}, []);
|
|
132
|
+
|
|
133
|
+
return searchParams;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Loader ๋ฐ์ดํฐ ์ ๊ทผ
|
|
138
|
+
*
|
|
139
|
+
* @example
|
|
140
|
+
* ```tsx
|
|
141
|
+
* interface UserData { name: string; email: string; }
|
|
142
|
+
* const data = useLoaderData<UserData>();
|
|
143
|
+
* ```
|
|
144
|
+
*/
|
|
145
|
+
export function useLoaderData<T = unknown>(): T | undefined {
|
|
146
|
+
const state = useRouterState();
|
|
147
|
+
return state.loaderData as T | undefined;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* ๋ค๋น๊ฒ์ด์
์ํ (๋ก๋ฉ ์ฌ๋ถ)
|
|
152
|
+
*
|
|
153
|
+
* @example
|
|
154
|
+
* ```tsx
|
|
155
|
+
* const { state, location } = useNavigation();
|
|
156
|
+
*
|
|
157
|
+
* if (state === "loading") {
|
|
158
|
+
* return <Spinner />;
|
|
159
|
+
* }
|
|
160
|
+
* ```
|
|
161
|
+
*/
|
|
162
|
+
export function useNavigation(): NavigationState {
|
|
163
|
+
const state = useRouterState();
|
|
164
|
+
return state.navigation;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* ํ๋ก๊ทธ๋๋งคํฑ ๋ค๋น๊ฒ์ด์
|
|
169
|
+
*
|
|
170
|
+
* @example
|
|
171
|
+
* ```tsx
|
|
172
|
+
* const navigate = useNavigate();
|
|
173
|
+
*
|
|
174
|
+
* const handleClick = () => {
|
|
175
|
+
* navigate("/dashboard");
|
|
176
|
+
* };
|
|
177
|
+
*
|
|
178
|
+
* const handleSubmit = () => {
|
|
179
|
+
* navigate("/success", { replace: true });
|
|
180
|
+
* };
|
|
181
|
+
* ```
|
|
182
|
+
*/
|
|
183
|
+
export function useNavigate(): (to: string, options?: NavigateOptions) => Promise<void> {
|
|
184
|
+
return useCallback((to: string, options?: NavigateOptions) => {
|
|
185
|
+
return navigate(to, options);
|
|
186
|
+
}, []);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* ๋ผ์ฐํฐ ํตํฉ ํ
(ํธ์์ฉ)
|
|
191
|
+
*
|
|
192
|
+
* @example
|
|
193
|
+
* ```tsx
|
|
194
|
+
* const {
|
|
195
|
+
* pathname,
|
|
196
|
+
* params,
|
|
197
|
+
* searchParams,
|
|
198
|
+
* navigate,
|
|
199
|
+
* isNavigating
|
|
200
|
+
* } = useRouter();
|
|
201
|
+
* ```
|
|
202
|
+
*/
|
|
203
|
+
export function useRouter() {
|
|
204
|
+
const pathname = usePathname();
|
|
205
|
+
const params = useParams();
|
|
206
|
+
const searchParams = useSearchParams();
|
|
207
|
+
const navigation = useNavigation();
|
|
208
|
+
const navigateFn = useNavigate();
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
/** ํ์ฌ ๊ฒฝ๋ก๋ช
*/
|
|
212
|
+
pathname,
|
|
213
|
+
/** URL ํ๋ผ๋ฏธํฐ */
|
|
214
|
+
params,
|
|
215
|
+
/** ๊ฒ์ ํ๋ผ๋ฏธํฐ (์ฟผ๋ฆฌ ์คํธ๋ง) */
|
|
216
|
+
searchParams,
|
|
217
|
+
/** ๋ค๋น๊ฒ์ด์
ํจ์ */
|
|
218
|
+
navigate: navigateFn,
|
|
219
|
+
/** ๋ค๋น๊ฒ์ด์
์ค ์ฌ๋ถ */
|
|
220
|
+
isNavigating: navigation.state === "loading",
|
|
221
|
+
/** ๋ค๋น๊ฒ์ด์
์ํ ์์ธ */
|
|
222
|
+
navigation,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* ํน์ ๊ฒฝ๋ก์ ํ์ฌ ๊ฒฝ๋ก ์ผ์น ์ฌ๋ถ
|
|
228
|
+
*
|
|
229
|
+
* @example
|
|
230
|
+
* ```tsx
|
|
231
|
+
* const isActive = useMatch("/about");
|
|
232
|
+
* const isUsersPage = useMatch("/users/:id");
|
|
233
|
+
* ```
|
|
234
|
+
*/
|
|
235
|
+
export function useMatch(pattern: string): boolean {
|
|
236
|
+
const pathname = usePathname();
|
|
237
|
+
|
|
238
|
+
// ๊ฐ๋จํ ํจํด ๋งค์นญ (ํ๋ผ๋ฏธํฐ ๊ณ ๋ ค)
|
|
239
|
+
const regexStr = pattern
|
|
240
|
+
.replace(/:[a-zA-Z_][a-zA-Z0-9_]*/g, "[^/]+")
|
|
241
|
+
.replace(/\//g, "\\/");
|
|
242
|
+
|
|
243
|
+
const regex = new RegExp(`^${regexStr}$`);
|
|
244
|
+
return regex.test(pathname);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* ๋ค๋ก ๊ฐ๊ธฐ
|
|
249
|
+
*/
|
|
250
|
+
export function useGoBack(): () => void {
|
|
251
|
+
return useCallback(() => {
|
|
252
|
+
if (typeof window !== "undefined") {
|
|
253
|
+
window.history.back();
|
|
254
|
+
}
|
|
255
|
+
}, []);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* ์์ผ๋ก ๊ฐ๊ธฐ
|
|
260
|
+
*/
|
|
261
|
+
export function useGoForward(): () => void {
|
|
262
|
+
return useCallback(() => {
|
|
263
|
+
if (typeof window !== "undefined") {
|
|
264
|
+
window.history.forward();
|
|
265
|
+
}
|
|
266
|
+
}, []);
|
|
267
|
+
}
|
package/src/client/index.ts
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Mandu Client Module ๐๏ธ
|
|
3
|
-
* ํด๋ผ์ด์ธํธ ์ฌ์ด๋ hydration
|
|
3
|
+
* ํด๋ผ์ด์ธํธ ์ฌ์ด๋ hydration ๋ฐ ๋ผ์ฐํ
์ ์ํ API
|
|
4
4
|
*
|
|
5
5
|
* @example
|
|
6
6
|
* ```typescript
|
|
7
|
-
* //
|
|
7
|
+
* // Island ์ปดํฌ๋ํธ
|
|
8
8
|
* import { Mandu } from "@mandujs/core/client";
|
|
9
9
|
*
|
|
10
10
|
* export default Mandu.island<TodosData>({
|
|
@@ -12,6 +12,17 @@
|
|
|
12
12
|
* render: (props) => <TodoList {...props} />
|
|
13
13
|
* });
|
|
14
14
|
* ```
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```typescript
|
|
18
|
+
* // Client-side ๋ผ์ฐํ
|
|
19
|
+
* import { Link, useRouter } from "@mandujs/core/client";
|
|
20
|
+
*
|
|
21
|
+
* function Nav() {
|
|
22
|
+
* const { pathname, navigate } = useRouter();
|
|
23
|
+
* return <Link href="/about">About</Link>;
|
|
24
|
+
* }
|
|
25
|
+
* ```
|
|
15
26
|
*/
|
|
16
27
|
|
|
17
28
|
// Island API
|
|
@@ -42,9 +53,56 @@ export {
|
|
|
42
53
|
type IslandLoader,
|
|
43
54
|
} from "./runtime";
|
|
44
55
|
|
|
56
|
+
// Client-side Router API
|
|
57
|
+
export {
|
|
58
|
+
navigate,
|
|
59
|
+
prefetch,
|
|
60
|
+
subscribe,
|
|
61
|
+
getRouterState,
|
|
62
|
+
getCurrentRoute,
|
|
63
|
+
getLoaderData,
|
|
64
|
+
getNavigationState,
|
|
65
|
+
initializeRouter,
|
|
66
|
+
cleanupRouter,
|
|
67
|
+
type RouteInfo,
|
|
68
|
+
type NavigationState,
|
|
69
|
+
type RouterState,
|
|
70
|
+
type NavigateOptions,
|
|
71
|
+
} from "./router";
|
|
72
|
+
|
|
73
|
+
// Link Components
|
|
74
|
+
export { Link, NavLink, type LinkProps, type NavLinkProps } from "./Link";
|
|
75
|
+
|
|
76
|
+
// Router Hooks
|
|
77
|
+
export {
|
|
78
|
+
useRouter,
|
|
79
|
+
useRoute,
|
|
80
|
+
useParams,
|
|
81
|
+
usePathname,
|
|
82
|
+
useSearchParams,
|
|
83
|
+
useLoaderData,
|
|
84
|
+
useNavigation,
|
|
85
|
+
useNavigate,
|
|
86
|
+
useMatch,
|
|
87
|
+
useGoBack,
|
|
88
|
+
useGoForward,
|
|
89
|
+
useRouterState,
|
|
90
|
+
} from "./hooks";
|
|
91
|
+
|
|
92
|
+
// Props Serialization (Fresh ์คํ์ผ)
|
|
93
|
+
export {
|
|
94
|
+
serializeProps,
|
|
95
|
+
deserializeProps,
|
|
96
|
+
isSerializable,
|
|
97
|
+
generatePropsScript,
|
|
98
|
+
parsePropsScript,
|
|
99
|
+
} from "./serialize";
|
|
100
|
+
|
|
45
101
|
// Re-export as Mandu namespace for consistent API
|
|
46
102
|
import { island, wrapComponent } from "./island";
|
|
47
103
|
import { hydrateIslands, initializeRuntime } from "./runtime";
|
|
104
|
+
import { navigate, prefetch, initializeRouter } from "./router";
|
|
105
|
+
import { Link, NavLink } from "./Link";
|
|
48
106
|
|
|
49
107
|
/**
|
|
50
108
|
* Mandu Client namespace
|
|
@@ -73,4 +131,34 @@ export const Mandu = {
|
|
|
73
131
|
* @see initializeRuntime
|
|
74
132
|
*/
|
|
75
133
|
init: initializeRuntime,
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Navigate to a URL (client-side)
|
|
137
|
+
* @see navigate
|
|
138
|
+
*/
|
|
139
|
+
navigate,
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Prefetch a URL for faster navigation
|
|
143
|
+
* @see prefetch
|
|
144
|
+
*/
|
|
145
|
+
prefetch,
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Initialize the client-side router
|
|
149
|
+
* @see initializeRouter
|
|
150
|
+
*/
|
|
151
|
+
initRouter: initializeRouter,
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Link component for client-side navigation
|
|
155
|
+
* @see Link
|
|
156
|
+
*/
|
|
157
|
+
Link,
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* NavLink component with active state
|
|
161
|
+
* @see NavLink
|
|
162
|
+
*/
|
|
163
|
+
NavLink,
|
|
76
164
|
};
|