@mandujs/core 0.12.1 → 0.13.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/README.ko.md +304 -304
- package/README.md +653 -653
- package/package.json +8 -8
- package/src/brain/architecture/analyzer.ts +28 -26
- package/src/brain/doctor/analyzer.ts +1 -1
- package/src/bundler/build.ts +91 -91
- package/src/bundler/css.ts +302 -302
- package/src/bundler/dev.ts +0 -1
- package/src/change/history.ts +3 -3
- package/src/change/snapshot.ts +10 -9
- package/src/change/transaction.ts +2 -2
- package/src/client/Link.tsx +227 -227
- package/src/client/globals.ts +44 -44
- package/src/client/hooks.ts +267 -267
- package/src/client/index.ts +5 -5
- package/src/client/island.ts +8 -8
- package/src/client/router.ts +435 -435
- package/src/client/runtime.ts +23 -23
- package/src/client/serialize.ts +404 -404
- package/src/client/window-state.ts +101 -101
- package/src/config/mandu.ts +94 -96
- package/src/config/validate.ts +213 -215
- package/src/config/watcher.ts +311 -311
- package/src/constants.ts +40 -40
- package/src/content/content-layer.ts +314 -314
- package/src/content/content.test.ts +433 -433
- package/src/content/data-store.ts +245 -245
- package/src/content/digest.ts +133 -133
- package/src/content/index.ts +164 -164
- package/src/content/loader-context.ts +172 -172
- package/src/content/loaders/api.ts +216 -216
- package/src/content/loaders/file.ts +169 -169
- package/src/content/loaders/glob.ts +252 -252
- package/src/content/loaders/index.ts +34 -34
- package/src/content/loaders/types.ts +137 -137
- package/src/content/meta-store.ts +209 -209
- package/src/content/types.ts +282 -282
- package/src/content/watcher.ts +135 -135
- package/src/contract/client-safe.test.ts +42 -42
- package/src/contract/client-safe.ts +114 -114
- package/src/contract/client.ts +16 -16
- package/src/contract/define.ts +459 -459
- package/src/contract/handler.ts +10 -10
- package/src/contract/normalize.test.ts +276 -276
- package/src/contract/normalize.ts +404 -404
- package/src/contract/registry.test.ts +206 -206
- package/src/contract/registry.ts +568 -568
- package/src/contract/schema.ts +48 -48
- package/src/contract/types.ts +58 -58
- package/src/contract/validator.ts +32 -32
- package/src/devtools/ai/context-builder.ts +375 -375
- package/src/devtools/ai/index.ts +25 -25
- package/src/devtools/ai/mcp-connector.ts +465 -465
- package/src/devtools/client/catchers/error-catcher.ts +327 -327
- package/src/devtools/client/catchers/index.ts +18 -18
- package/src/devtools/client/catchers/network-proxy.ts +363 -363
- package/src/devtools/client/components/index.ts +39 -39
- package/src/devtools/client/components/kitchen-root.tsx +362 -362
- package/src/devtools/client/components/mandu-character.tsx +241 -241
- package/src/devtools/client/components/overlay.tsx +368 -368
- package/src/devtools/client/components/panel/errors-panel.tsx +259 -259
- package/src/devtools/client/components/panel/guard-panel.tsx +244 -244
- package/src/devtools/client/components/panel/index.ts +32 -32
- package/src/devtools/client/components/panel/islands-panel.tsx +304 -304
- package/src/devtools/client/components/panel/network-panel.tsx +292 -292
- package/src/devtools/client/components/panel/panel-container.tsx +259 -259
- package/src/devtools/client/filters/context-filters.ts +282 -282
- package/src/devtools/client/filters/index.ts +16 -16
- package/src/devtools/client/index.ts +63 -63
- package/src/devtools/client/persistence.ts +335 -335
- package/src/devtools/client/state-manager.ts +478 -478
- package/src/devtools/design-tokens.ts +263 -263
- package/src/devtools/hook/create-hook.ts +207 -207
- package/src/devtools/hook/index.ts +13 -13
- package/src/devtools/index.ts +439 -439
- package/src/devtools/init.ts +266 -266
- package/src/devtools/protocol.ts +237 -237
- package/src/devtools/server/index.ts +17 -17
- package/src/devtools/server/source-context.ts +444 -444
- package/src/devtools/types.ts +319 -319
- package/src/devtools/worker/index.ts +25 -25
- package/src/devtools/worker/redaction-worker.ts +222 -222
- package/src/devtools/worker/worker-manager.ts +409 -409
- package/src/error/classifier.ts +2 -2
- package/src/error/domains.ts +265 -265
- package/src/error/formatter.ts +32 -32
- package/src/error/result.ts +46 -46
- package/src/error/stack-analyzer.ts +5 -0
- package/src/error/types.ts +6 -6
- package/src/errors/extractor.ts +409 -409
- package/src/errors/index.ts +19 -19
- package/src/filling/auth.ts +308 -308
- package/src/filling/context.ts +569 -569
- package/src/filling/deps.ts +238 -238
- package/src/generator/contract-glue.ts +2 -1
- package/src/generator/generate.ts +12 -10
- package/src/generator/index.ts +3 -3
- package/src/generator/templates.ts +80 -79
- package/src/guard/analyzer.ts +360 -360
- package/src/guard/ast-analyzer.ts +806 -806
- package/src/guard/auto-correct.ts +1 -1
- package/src/guard/check.ts +128 -128
- package/src/guard/contract-guard.ts +9 -9
- package/src/guard/file-type.test.ts +24 -24
- package/src/guard/healing.ts +2 -0
- package/src/guard/index.ts +2 -0
- package/src/guard/negotiation.ts +430 -4
- package/src/guard/presets/atomic.ts +70 -70
- package/src/guard/presets/clean.ts +77 -77
- package/src/guard/presets/cqrs.test.ts +175 -0
- package/src/guard/presets/cqrs.ts +107 -0
- package/src/guard/presets/fsd.ts +79 -79
- package/src/guard/presets/hexagonal.ts +68 -68
- package/src/guard/presets/index.ts +291 -288
- package/src/guard/reporter.ts +445 -445
- package/src/guard/rules.ts +12 -12
- package/src/guard/statistics.ts +578 -578
- package/src/guard/suggestions.ts +358 -352
- package/src/guard/types.ts +348 -347
- package/src/guard/validator.ts +834 -834
- package/src/guard/watcher.ts +404 -404
- package/src/index.ts +1 -0
- package/src/intent/index.ts +310 -310
- package/src/island/index.ts +304 -304
- package/src/logging/index.ts +22 -22
- package/src/logging/transports.ts +365 -365
- package/src/paths.test.ts +47 -0
- package/src/paths.ts +47 -0
- package/src/plugins/index.ts +38 -38
- package/src/plugins/registry.ts +377 -377
- package/src/plugins/types.ts +363 -363
- package/src/report/build.ts +1 -1
- package/src/report/index.ts +1 -1
- package/src/router/fs-patterns.ts +387 -387
- package/src/router/fs-routes.ts +344 -401
- package/src/router/fs-scanner.ts +497 -497
- package/src/router/fs-types.ts +270 -278
- package/src/router/index.ts +81 -81
- package/src/runtime/boundary.tsx +232 -232
- package/src/runtime/compose.ts +222 -222
- package/src/runtime/lifecycle.ts +381 -381
- package/src/runtime/logger.test.ts +345 -345
- package/src/runtime/logger.ts +677 -677
- package/src/runtime/router.test.ts +476 -476
- package/src/runtime/router.ts +105 -105
- package/src/runtime/security.ts +155 -155
- package/src/runtime/server.ts +24 -24
- package/src/runtime/session-key.ts +328 -328
- package/src/runtime/ssr.ts +367 -367
- package/src/runtime/streaming-ssr.ts +1245 -1245
- package/src/runtime/trace.ts +144 -144
- package/src/seo/index.ts +214 -214
- package/src/seo/integration/ssr.ts +307 -307
- package/src/seo/render/basic.ts +427 -427
- package/src/seo/render/index.ts +143 -143
- package/src/seo/render/jsonld.ts +539 -539
- package/src/seo/render/opengraph.ts +191 -191
- package/src/seo/render/robots.ts +116 -116
- package/src/seo/render/sitemap.ts +137 -137
- package/src/seo/render/twitter.ts +126 -126
- package/src/seo/resolve/index.ts +353 -353
- package/src/seo/resolve/opengraph.ts +143 -143
- package/src/seo/resolve/robots.ts +73 -73
- package/src/seo/resolve/title.ts +94 -94
- package/src/seo/resolve/twitter.ts +73 -73
- package/src/seo/resolve/url.ts +97 -97
- package/src/seo/routes/index.ts +290 -290
- package/src/seo/types.ts +575 -575
- package/src/slot/validator.ts +39 -39
- package/src/spec/index.ts +3 -3
- package/src/spec/load.ts +76 -76
- package/src/spec/lock.ts +56 -56
- package/src/utils/bun.ts +8 -8
- package/src/utils/lru-cache.ts +75 -75
- package/src/utils/safe-io.ts +188 -188
- package/src/utils/string-safe.ts +298 -298
- package/src/watcher/rules.ts +5 -5
package/src/change/snapshot.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
} from "../lockfile";
|
|
12
12
|
import { validateAndReport } from "../config";
|
|
13
13
|
|
|
14
|
+
const MANDU_DIR = ".mandu";
|
|
14
15
|
const SPEC_DIR = "spec";
|
|
15
16
|
const MANIFEST_FILE = "routes.manifest.json";
|
|
16
17
|
const LOCK_FILE = "spec.lock.json";
|
|
@@ -33,7 +34,7 @@ function generateSnapshotId(): string {
|
|
|
33
34
|
* 스냅샷 저장 경로 반환
|
|
34
35
|
*/
|
|
35
36
|
function getSnapshotPath(rootDir: string, snapshotId: string): string {
|
|
36
|
-
return path.join(rootDir,
|
|
37
|
+
return path.join(rootDir, MANDU_DIR, HISTORY_DIR, SNAPSHOTS_DIR, `${snapshotId}.snapshot.json`);
|
|
37
38
|
}
|
|
38
39
|
|
|
39
40
|
/**
|
|
@@ -71,9 +72,9 @@ async function collectSlotContents(rootDir: string): Promise<Record<string, stri
|
|
|
71
72
|
* 현재 spec 상태의 스냅샷 생성
|
|
72
73
|
*/
|
|
73
74
|
export async function createSnapshot(rootDir: string): Promise<Snapshot> {
|
|
74
|
-
const
|
|
75
|
-
const manifestPath = path.join(
|
|
76
|
-
const lockPath = path.join(
|
|
75
|
+
const manduDir = path.join(rootDir, MANDU_DIR);
|
|
76
|
+
const manifestPath = path.join(manduDir, MANIFEST_FILE);
|
|
77
|
+
const lockPath = path.join(manduDir, LOCK_FILE);
|
|
77
78
|
|
|
78
79
|
// Manifest 읽기 (필수)
|
|
79
80
|
const manifestFile = Bun.file(manifestPath);
|
|
@@ -168,10 +169,10 @@ export async function readSnapshotById(rootDir: string, snapshotId: string): Pro
|
|
|
168
169
|
* 스냅샷으로부터 상태 복원
|
|
169
170
|
*/
|
|
170
171
|
export async function restoreSnapshot(rootDir: string, snapshot: Snapshot): Promise<RestoreResult> {
|
|
171
|
-
const
|
|
172
|
-
const manifestPath = path.join(
|
|
173
|
-
const lockPath = path.join(
|
|
174
|
-
const slotsDir = path.join(
|
|
172
|
+
const manduDir = path.join(rootDir, MANDU_DIR);
|
|
173
|
+
const manifestPath = path.join(manduDir, MANIFEST_FILE);
|
|
174
|
+
const lockPath = path.join(manduDir, LOCK_FILE);
|
|
175
|
+
const slotsDir = path.join(rootDir, SPEC_DIR, SLOTS_DIR);
|
|
175
176
|
|
|
176
177
|
const restoredFiles: string[] = [];
|
|
177
178
|
const failedFiles: string[] = [];
|
|
@@ -258,7 +259,7 @@ export async function deleteSnapshot(rootDir: string, snapshotId: string): Promi
|
|
|
258
259
|
* 모든 스냅샷 ID 목록 조회
|
|
259
260
|
*/
|
|
260
261
|
export async function listSnapshotIds(rootDir: string): Promise<string[]> {
|
|
261
|
-
const snapshotsDir = path.join(rootDir,
|
|
262
|
+
const snapshotsDir = path.join(rootDir, MANDU_DIR, HISTORY_DIR, SNAPSHOTS_DIR);
|
|
262
263
|
|
|
263
264
|
try {
|
|
264
265
|
const entries = await Array.fromAsync(
|
|
@@ -8,7 +8,7 @@ import type {
|
|
|
8
8
|
} from "./types";
|
|
9
9
|
import { createSnapshot, writeSnapshot, readSnapshotById, restoreSnapshot } from "./snapshot";
|
|
10
10
|
|
|
11
|
-
const
|
|
11
|
+
const MANDU_DIR = ".mandu";
|
|
12
12
|
const HISTORY_DIR = "history";
|
|
13
13
|
const CHANGES_FILE = "changes.json";
|
|
14
14
|
const ACTIVE_FILE = "active.json";
|
|
@@ -28,7 +28,7 @@ function generateChangeId(): string {
|
|
|
28
28
|
* History 디렉토리 경로
|
|
29
29
|
*/
|
|
30
30
|
function getHistoryDir(rootDir: string): string {
|
|
31
|
-
return path.join(rootDir,
|
|
31
|
+
return path.join(rootDir, MANDU_DIR, HISTORY_DIR);
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
/**
|
package/src/client/Link.tsx
CHANGED
|
@@ -1,227 +1,227 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Mandu Link Component 🔗
|
|
3
|
-
* Client-side 네비게이션을 위한 Link 컴포넌트
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import React, {
|
|
7
|
-
type AnchorHTMLAttributes,
|
|
8
|
-
type MouseEvent,
|
|
9
|
-
type ReactNode,
|
|
10
|
-
useCallback,
|
|
11
|
-
useEffect,
|
|
12
|
-
useRef,
|
|
13
|
-
} from "react";
|
|
14
|
-
import { navigate, prefetch } from "./router";
|
|
15
|
-
|
|
16
|
-
export interface LinkProps
|
|
17
|
-
extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, "href"> {
|
|
18
|
-
/** 이동할 URL */
|
|
19
|
-
href: string;
|
|
20
|
-
/** history.replaceState 사용 여부 */
|
|
21
|
-
replace?: boolean;
|
|
22
|
-
/** 마우스 hover 시 prefetch 여부 */
|
|
23
|
-
prefetch?: boolean;
|
|
24
|
-
/** 스크롤 위치 복원 여부 (기본: true) */
|
|
25
|
-
scroll?: boolean;
|
|
26
|
-
/** 자식 요소 */
|
|
27
|
-
children?: ReactNode;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Client-side 네비게이션 Link 컴포넌트
|
|
32
|
-
*
|
|
33
|
-
* @example
|
|
34
|
-
* ```tsx
|
|
35
|
-
* import { Link } from "@mandujs/core/client";
|
|
36
|
-
*
|
|
37
|
-
* // 기본 사용
|
|
38
|
-
* <Link href="/about">About</Link>
|
|
39
|
-
*
|
|
40
|
-
* // Prefetch 활성화
|
|
41
|
-
* <Link href="/users" prefetch>Users</Link>
|
|
42
|
-
*
|
|
43
|
-
* // Replace 모드 (뒤로가기 히스토리 없음)
|
|
44
|
-
* <Link href="/login" replace>Login</Link>
|
|
45
|
-
* ```
|
|
46
|
-
*/
|
|
47
|
-
export function Link({
|
|
48
|
-
href,
|
|
49
|
-
replace = false,
|
|
50
|
-
prefetch: shouldPrefetch = false,
|
|
51
|
-
scroll = true,
|
|
52
|
-
children,
|
|
53
|
-
onClick,
|
|
54
|
-
onMouseEnter,
|
|
55
|
-
onFocus,
|
|
56
|
-
...rest
|
|
57
|
-
}: LinkProps): React.ReactElement {
|
|
58
|
-
const prefetchedRef = useRef(false);
|
|
59
|
-
|
|
60
|
-
// 클릭 핸들러
|
|
61
|
-
const handleClick = useCallback(
|
|
62
|
-
(event: MouseEvent<HTMLAnchorElement>) => {
|
|
63
|
-
// 사용자 정의 onClick 먼저 실행
|
|
64
|
-
onClick?.(event);
|
|
65
|
-
|
|
66
|
-
// 기본 동작 방지 조건
|
|
67
|
-
if (
|
|
68
|
-
event.defaultPrevented ||
|
|
69
|
-
event.button !== 0 ||
|
|
70
|
-
event.metaKey ||
|
|
71
|
-
event.altKey ||
|
|
72
|
-
event.ctrlKey ||
|
|
73
|
-
event.shiftKey
|
|
74
|
-
) {
|
|
75
|
-
return;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// 외부 링크 체크
|
|
79
|
-
try {
|
|
80
|
-
const url = new URL(href, window.location.origin);
|
|
81
|
-
if (url.origin !== window.location.origin) {
|
|
82
|
-
return; // 외부 링크는 기본 동작
|
|
83
|
-
}
|
|
84
|
-
} catch {
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// Client-side 네비게이션
|
|
89
|
-
event.preventDefault();
|
|
90
|
-
navigate(href, { replace, scroll });
|
|
91
|
-
},
|
|
92
|
-
[href, replace, scroll, onClick]
|
|
93
|
-
);
|
|
94
|
-
|
|
95
|
-
// Prefetch 실행
|
|
96
|
-
const doPrefetch = useCallback(() => {
|
|
97
|
-
if (!shouldPrefetch || prefetchedRef.current) return;
|
|
98
|
-
|
|
99
|
-
try {
|
|
100
|
-
const url = new URL(href, window.location.origin);
|
|
101
|
-
if (url.origin === window.location.origin) {
|
|
102
|
-
prefetch(href);
|
|
103
|
-
prefetchedRef.current = true;
|
|
104
|
-
}
|
|
105
|
-
} catch {
|
|
106
|
-
// 무시
|
|
107
|
-
}
|
|
108
|
-
}, [href, shouldPrefetch]);
|
|
109
|
-
|
|
110
|
-
// 마우스 hover 핸들러
|
|
111
|
-
const handleMouseEnter = useCallback(
|
|
112
|
-
(event: MouseEvent<HTMLAnchorElement>) => {
|
|
113
|
-
onMouseEnter?.(event);
|
|
114
|
-
doPrefetch();
|
|
115
|
-
},
|
|
116
|
-
[onMouseEnter, doPrefetch]
|
|
117
|
-
);
|
|
118
|
-
|
|
119
|
-
// 포커스 핸들러 (키보드 네비게이션)
|
|
120
|
-
const handleFocus = useCallback(
|
|
121
|
-
(event: React.FocusEvent<HTMLAnchorElement>) => {
|
|
122
|
-
onFocus?.(event);
|
|
123
|
-
doPrefetch();
|
|
124
|
-
},
|
|
125
|
-
[onFocus, doPrefetch]
|
|
126
|
-
);
|
|
127
|
-
|
|
128
|
-
// Viewport 진입 시 prefetch (IntersectionObserver)
|
|
129
|
-
useEffect(() => {
|
|
130
|
-
if (!shouldPrefetch || typeof IntersectionObserver === "undefined") {
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// ref가 없으면 무시 (SSR)
|
|
135
|
-
return;
|
|
136
|
-
}, [shouldPrefetch]);
|
|
137
|
-
|
|
138
|
-
return (
|
|
139
|
-
<a
|
|
140
|
-
href={href}
|
|
141
|
-
onClick={handleClick}
|
|
142
|
-
onMouseEnter={handleMouseEnter}
|
|
143
|
-
onFocus={handleFocus}
|
|
144
|
-
data-mandu-link=""
|
|
145
|
-
{...rest}
|
|
146
|
-
>
|
|
147
|
-
{children}
|
|
148
|
-
</a>
|
|
149
|
-
);
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* NavLink - 현재 경로와 일치할 때 활성 스타일 적용
|
|
154
|
-
*
|
|
155
|
-
* @example
|
|
156
|
-
* ```tsx
|
|
157
|
-
* import { NavLink } from "@mandujs/core/client";
|
|
158
|
-
*
|
|
159
|
-
* <NavLink
|
|
160
|
-
* href="/about"
|
|
161
|
-
* className={({ isActive }) => isActive ? "active" : ""}
|
|
162
|
-
* >
|
|
163
|
-
* About
|
|
164
|
-
* </NavLink>
|
|
165
|
-
* ```
|
|
166
|
-
*/
|
|
167
|
-
export interface NavLinkProps extends Omit<LinkProps, "className" | "style"> {
|
|
168
|
-
/** 활성 상태에 따른 className */
|
|
169
|
-
className?: string | ((props: { isActive: boolean }) => string);
|
|
170
|
-
/** 활성 상태에 따른 style */
|
|
171
|
-
style?:
|
|
172
|
-
| React.CSSProperties
|
|
173
|
-
| ((props: { isActive: boolean }) => React.CSSProperties);
|
|
174
|
-
/** 활성 상태일 때 적용할 style (style과 병합됨) */
|
|
175
|
-
activeStyle?: React.CSSProperties;
|
|
176
|
-
/** 활성 상태일 때 추가할 className */
|
|
177
|
-
activeClassName?: string;
|
|
178
|
-
/** 정확히 일치해야 활성화 (기본: false) */
|
|
179
|
-
exact?: boolean;
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
export function NavLink({
|
|
183
|
-
href,
|
|
184
|
-
className,
|
|
185
|
-
style,
|
|
186
|
-
activeStyle,
|
|
187
|
-
activeClassName,
|
|
188
|
-
exact = false,
|
|
189
|
-
...rest
|
|
190
|
-
}: NavLinkProps): React.ReactElement {
|
|
191
|
-
// 현재 경로와 비교
|
|
192
|
-
const isActive =
|
|
193
|
-
typeof window !== "undefined"
|
|
194
|
-
? exact
|
|
195
|
-
? window.location.pathname === href
|
|
196
|
-
: window.location.pathname.startsWith(href)
|
|
197
|
-
: false;
|
|
198
|
-
|
|
199
|
-
// className 처리
|
|
200
|
-
let resolvedClassName =
|
|
201
|
-
typeof className === "function" ? className({ isActive }) : className;
|
|
202
|
-
|
|
203
|
-
if (isActive && activeClassName) {
|
|
204
|
-
resolvedClassName = resolvedClassName
|
|
205
|
-
? `${resolvedClassName} ${activeClassName}`
|
|
206
|
-
: activeClassName;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// style 처리
|
|
210
|
-
let resolvedStyle =
|
|
211
|
-
typeof style === "function" ? style({ isActive }) : style;
|
|
212
|
-
|
|
213
|
-
if (isActive && activeStyle) {
|
|
214
|
-
resolvedStyle = { ...resolvedStyle, ...activeStyle };
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
return (
|
|
218
|
-
<Link
|
|
219
|
-
href={href}
|
|
220
|
-
className={resolvedClassName}
|
|
221
|
-
style={resolvedStyle}
|
|
222
|
-
{...rest}
|
|
223
|
-
/>
|
|
224
|
-
);
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
export default Link;
|
|
1
|
+
/**
|
|
2
|
+
* Mandu Link Component 🔗
|
|
3
|
+
* Client-side 네비게이션을 위한 Link 컴포넌트
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import React, {
|
|
7
|
+
type AnchorHTMLAttributes,
|
|
8
|
+
type MouseEvent,
|
|
9
|
+
type ReactNode,
|
|
10
|
+
useCallback,
|
|
11
|
+
useEffect,
|
|
12
|
+
useRef,
|
|
13
|
+
} from "react";
|
|
14
|
+
import { navigate, prefetch } from "./router";
|
|
15
|
+
|
|
16
|
+
export interface LinkProps
|
|
17
|
+
extends Omit<AnchorHTMLAttributes<HTMLAnchorElement>, "href"> {
|
|
18
|
+
/** 이동할 URL */
|
|
19
|
+
href: string;
|
|
20
|
+
/** history.replaceState 사용 여부 */
|
|
21
|
+
replace?: boolean;
|
|
22
|
+
/** 마우스 hover 시 prefetch 여부 */
|
|
23
|
+
prefetch?: boolean;
|
|
24
|
+
/** 스크롤 위치 복원 여부 (기본: true) */
|
|
25
|
+
scroll?: boolean;
|
|
26
|
+
/** 자식 요소 */
|
|
27
|
+
children?: ReactNode;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Client-side 네비게이션 Link 컴포넌트
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```tsx
|
|
35
|
+
* import { Link } from "@mandujs/core/client";
|
|
36
|
+
*
|
|
37
|
+
* // 기본 사용
|
|
38
|
+
* <Link href="/about">About</Link>
|
|
39
|
+
*
|
|
40
|
+
* // Prefetch 활성화
|
|
41
|
+
* <Link href="/users" prefetch>Users</Link>
|
|
42
|
+
*
|
|
43
|
+
* // Replace 모드 (뒤로가기 히스토리 없음)
|
|
44
|
+
* <Link href="/login" replace>Login</Link>
|
|
45
|
+
* ```
|
|
46
|
+
*/
|
|
47
|
+
export function Link({
|
|
48
|
+
href,
|
|
49
|
+
replace = false,
|
|
50
|
+
prefetch: shouldPrefetch = false,
|
|
51
|
+
scroll = true,
|
|
52
|
+
children,
|
|
53
|
+
onClick,
|
|
54
|
+
onMouseEnter,
|
|
55
|
+
onFocus,
|
|
56
|
+
...rest
|
|
57
|
+
}: LinkProps): React.ReactElement {
|
|
58
|
+
const prefetchedRef = useRef(false);
|
|
59
|
+
|
|
60
|
+
// 클릭 핸들러
|
|
61
|
+
const handleClick = useCallback(
|
|
62
|
+
(event: MouseEvent<HTMLAnchorElement>) => {
|
|
63
|
+
// 사용자 정의 onClick 먼저 실행
|
|
64
|
+
onClick?.(event);
|
|
65
|
+
|
|
66
|
+
// 기본 동작 방지 조건
|
|
67
|
+
if (
|
|
68
|
+
event.defaultPrevented ||
|
|
69
|
+
event.button !== 0 ||
|
|
70
|
+
event.metaKey ||
|
|
71
|
+
event.altKey ||
|
|
72
|
+
event.ctrlKey ||
|
|
73
|
+
event.shiftKey
|
|
74
|
+
) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// 외부 링크 체크
|
|
79
|
+
try {
|
|
80
|
+
const url = new URL(href, window.location.origin);
|
|
81
|
+
if (url.origin !== window.location.origin) {
|
|
82
|
+
return; // 외부 링크는 기본 동작
|
|
83
|
+
}
|
|
84
|
+
} catch {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Client-side 네비게이션
|
|
89
|
+
event.preventDefault();
|
|
90
|
+
navigate(href, { replace, scroll });
|
|
91
|
+
},
|
|
92
|
+
[href, replace, scroll, onClick]
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
// Prefetch 실행
|
|
96
|
+
const doPrefetch = useCallback(() => {
|
|
97
|
+
if (!shouldPrefetch || prefetchedRef.current) return;
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
const url = new URL(href, window.location.origin);
|
|
101
|
+
if (url.origin === window.location.origin) {
|
|
102
|
+
prefetch(href);
|
|
103
|
+
prefetchedRef.current = true;
|
|
104
|
+
}
|
|
105
|
+
} catch {
|
|
106
|
+
// 무시
|
|
107
|
+
}
|
|
108
|
+
}, [href, shouldPrefetch]);
|
|
109
|
+
|
|
110
|
+
// 마우스 hover 핸들러
|
|
111
|
+
const handleMouseEnter = useCallback(
|
|
112
|
+
(event: MouseEvent<HTMLAnchorElement>) => {
|
|
113
|
+
onMouseEnter?.(event);
|
|
114
|
+
doPrefetch();
|
|
115
|
+
},
|
|
116
|
+
[onMouseEnter, doPrefetch]
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
// 포커스 핸들러 (키보드 네비게이션)
|
|
120
|
+
const handleFocus = useCallback(
|
|
121
|
+
(event: React.FocusEvent<HTMLAnchorElement>) => {
|
|
122
|
+
onFocus?.(event);
|
|
123
|
+
doPrefetch();
|
|
124
|
+
},
|
|
125
|
+
[onFocus, doPrefetch]
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
// Viewport 진입 시 prefetch (IntersectionObserver)
|
|
129
|
+
useEffect(() => {
|
|
130
|
+
if (!shouldPrefetch || typeof IntersectionObserver === "undefined") {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ref가 없으면 무시 (SSR)
|
|
135
|
+
return;
|
|
136
|
+
}, [shouldPrefetch]);
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
<a
|
|
140
|
+
href={href}
|
|
141
|
+
onClick={handleClick}
|
|
142
|
+
onMouseEnter={handleMouseEnter}
|
|
143
|
+
onFocus={handleFocus}
|
|
144
|
+
data-mandu-link=""
|
|
145
|
+
{...rest}
|
|
146
|
+
>
|
|
147
|
+
{children}
|
|
148
|
+
</a>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* NavLink - 현재 경로와 일치할 때 활성 스타일 적용
|
|
154
|
+
*
|
|
155
|
+
* @example
|
|
156
|
+
* ```tsx
|
|
157
|
+
* import { NavLink } from "@mandujs/core/client";
|
|
158
|
+
*
|
|
159
|
+
* <NavLink
|
|
160
|
+
* href="/about"
|
|
161
|
+
* className={({ isActive }) => isActive ? "active" : ""}
|
|
162
|
+
* >
|
|
163
|
+
* About
|
|
164
|
+
* </NavLink>
|
|
165
|
+
* ```
|
|
166
|
+
*/
|
|
167
|
+
export interface NavLinkProps extends Omit<LinkProps, "className" | "style"> {
|
|
168
|
+
/** 활성 상태에 따른 className */
|
|
169
|
+
className?: string | ((props: { isActive: boolean }) => string);
|
|
170
|
+
/** 활성 상태에 따른 style */
|
|
171
|
+
style?:
|
|
172
|
+
| React.CSSProperties
|
|
173
|
+
| ((props: { isActive: boolean }) => React.CSSProperties);
|
|
174
|
+
/** 활성 상태일 때 적용할 style (style과 병합됨) */
|
|
175
|
+
activeStyle?: React.CSSProperties;
|
|
176
|
+
/** 활성 상태일 때 추가할 className */
|
|
177
|
+
activeClassName?: string;
|
|
178
|
+
/** 정확히 일치해야 활성화 (기본: false) */
|
|
179
|
+
exact?: boolean;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function NavLink({
|
|
183
|
+
href,
|
|
184
|
+
className,
|
|
185
|
+
style,
|
|
186
|
+
activeStyle,
|
|
187
|
+
activeClassName,
|
|
188
|
+
exact = false,
|
|
189
|
+
...rest
|
|
190
|
+
}: NavLinkProps): React.ReactElement {
|
|
191
|
+
// 현재 경로와 비교
|
|
192
|
+
const isActive =
|
|
193
|
+
typeof window !== "undefined"
|
|
194
|
+
? exact
|
|
195
|
+
? window.location.pathname === href
|
|
196
|
+
: window.location.pathname.startsWith(href)
|
|
197
|
+
: false;
|
|
198
|
+
|
|
199
|
+
// className 처리
|
|
200
|
+
let resolvedClassName =
|
|
201
|
+
typeof className === "function" ? className({ isActive }) : className;
|
|
202
|
+
|
|
203
|
+
if (isActive && activeClassName) {
|
|
204
|
+
resolvedClassName = resolvedClassName
|
|
205
|
+
? `${resolvedClassName} ${activeClassName}`
|
|
206
|
+
: activeClassName;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// style 처리
|
|
210
|
+
let resolvedStyle =
|
|
211
|
+
typeof style === "function" ? style({ isActive }) : style;
|
|
212
|
+
|
|
213
|
+
if (isActive && activeStyle) {
|
|
214
|
+
resolvedStyle = { ...resolvedStyle, ...activeStyle };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return (
|
|
218
|
+
<Link
|
|
219
|
+
href={href}
|
|
220
|
+
className={resolvedClassName}
|
|
221
|
+
style={resolvedStyle}
|
|
222
|
+
{...rest}
|
|
223
|
+
/>
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export default Link;
|
package/src/client/globals.ts
CHANGED
|
@@ -1,44 +1,44 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Mandu 전역 타입 선언
|
|
3
|
-
* 클라이언트 측 전역 상태의 타입 정의
|
|
4
|
-
*/
|
|
5
|
-
import type { Root } from "react-dom/client";
|
|
6
|
-
import type { RouterState } from "./router";
|
|
7
|
-
|
|
8
|
-
interface ManduRouteInfo {
|
|
9
|
-
id: string;
|
|
10
|
-
pattern: string;
|
|
11
|
-
params: Record<string, string>;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
interface ManduDataEntry {
|
|
15
|
-
serverData: unknown;
|
|
16
|
-
timestamp?: number;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
declare global {
|
|
20
|
-
interface Window {
|
|
21
|
-
/** 서버에서 전달된 데이터 (routeId → data) */
|
|
22
|
-
__MANDU_DATA__?: Record<string, ManduDataEntry>;
|
|
23
|
-
|
|
24
|
-
/** 직렬화된 서버 데이터 (raw JSON) */
|
|
25
|
-
__MANDU_DATA_RAW__?: string;
|
|
26
|
-
|
|
27
|
-
/** 현재 라우트 정보 */
|
|
28
|
-
__MANDU_ROUTE__?: ManduRouteInfo;
|
|
29
|
-
|
|
30
|
-
/** 클라이언트 라우터 상태 */
|
|
31
|
-
__MANDU_ROUTER_STATE__?: RouterState;
|
|
32
|
-
|
|
33
|
-
/** 라우터 상태 변경 리스너 */
|
|
34
|
-
__MANDU_ROUTER_LISTENERS__?: Set<(state: RouterState) => void>;
|
|
35
|
-
|
|
36
|
-
/** Hydrated roots 추적 (unmount용) */
|
|
37
|
-
__MANDU_ROOTS__?: Map<string, Root>;
|
|
38
|
-
|
|
39
|
-
/** React 인스턴스 공유 */
|
|
40
|
-
__MANDU_REACT__?: typeof import("react");
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export {};
|
|
1
|
+
/**
|
|
2
|
+
* Mandu 전역 타입 선언
|
|
3
|
+
* 클라이언트 측 전역 상태의 타입 정의
|
|
4
|
+
*/
|
|
5
|
+
import type { Root } from "react-dom/client";
|
|
6
|
+
import type { RouterState } from "./router";
|
|
7
|
+
|
|
8
|
+
interface ManduRouteInfo {
|
|
9
|
+
id: string;
|
|
10
|
+
pattern: string;
|
|
11
|
+
params: Record<string, string>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
interface ManduDataEntry {
|
|
15
|
+
serverData: unknown;
|
|
16
|
+
timestamp?: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
declare global {
|
|
20
|
+
interface Window {
|
|
21
|
+
/** 서버에서 전달된 데이터 (routeId → data) */
|
|
22
|
+
__MANDU_DATA__?: Record<string, ManduDataEntry>;
|
|
23
|
+
|
|
24
|
+
/** 직렬화된 서버 데이터 (raw JSON) */
|
|
25
|
+
__MANDU_DATA_RAW__?: string;
|
|
26
|
+
|
|
27
|
+
/** 현재 라우트 정보 */
|
|
28
|
+
__MANDU_ROUTE__?: ManduRouteInfo;
|
|
29
|
+
|
|
30
|
+
/** 클라이언트 라우터 상태 */
|
|
31
|
+
__MANDU_ROUTER_STATE__?: RouterState;
|
|
32
|
+
|
|
33
|
+
/** 라우터 상태 변경 리스너 */
|
|
34
|
+
__MANDU_ROUTER_LISTENERS__?: Set<(state: RouterState) => void>;
|
|
35
|
+
|
|
36
|
+
/** Hydrated roots 추적 (unmount용) */
|
|
37
|
+
__MANDU_ROOTS__?: Map<string, Root>;
|
|
38
|
+
|
|
39
|
+
/** React 인스턴스 공유 */
|
|
40
|
+
__MANDU_REACT__?: typeof import("react");
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export {};
|