@mandujs/core 0.21.0 → 0.22.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.
- package/package.json +101 -69
- package/src/auth/__tests__/login.test.ts +419 -0
- package/src/auth/__tests__/password.test.ts +122 -0
- package/src/auth/__tests__/reset.test.ts +296 -0
- package/src/auth/__tests__/tokens.test.ts +274 -0
- package/src/auth/__tests__/verification.test.ts +274 -0
- package/src/auth/index.ts +76 -0
- package/src/auth/login.ts +225 -0
- package/src/auth/password.ts +120 -0
- package/src/auth/reset.ts +243 -0
- package/src/auth/tokens.ts +612 -0
- package/src/auth/verification.ts +253 -0
- package/src/bundler/__tests__/cli-bench-utils.test.ts +149 -0
- package/src/bundler/__tests__/cold-start.test.ts +504 -0
- package/src/bundler/__tests__/csp-nonce.test.ts +278 -0
- package/src/bundler/__tests__/dev-reliability.test.ts +619 -0
- package/src/bundler/__tests__/extended-watch.test.ts +710 -0
- package/src/bundler/__tests__/fast-refresh.test.ts +596 -0
- package/src/bundler/__tests__/hdr.test.ts +353 -0
- package/src/bundler/__tests__/hmr-client.test.ts +532 -0
- package/src/bundler/__tests__/manifest-schema.test.ts +266 -0
- package/src/bundler/__tests__/prod-smoke.test.ts +138 -0
- package/src/bundler/__tests__/slot-dispatch.test.ts +573 -0
- package/src/bundler/__tests__/url-cap-and-slot-regex.test.ts +286 -0
- package/src/bundler/__tests__/vendor-cache.test.ts +455 -0
- package/src/bundler/build.test.ts +8 -1
- package/src/bundler/build.ts +310 -18
- package/src/bundler/css.ts +326 -323
- package/src/bundler/dev.ts +1611 -59
- package/src/bundler/fast-refresh-plugin.ts +307 -0
- package/src/bundler/hmr-types.ts +252 -0
- package/src/bundler/manifest-schema.ts +301 -0
- package/src/bundler/safe-build.test.ts +128 -0
- package/src/bundler/safe-build.ts +77 -0
- package/src/bundler/scenario-matrix.ts +229 -0
- package/src/bundler/types.ts +11 -0
- package/src/bundler/vendor-cache-types.ts +130 -0
- package/src/bundler/vendor-cache.ts +526 -0
- package/src/client/router.ts +214 -56
- package/src/db/__tests__/db.test.ts +485 -0
- package/src/db/index.ts +513 -0
- package/src/db/migrations/__tests__/runner.test.ts +661 -0
- package/src/db/migrations/history-table.ts +345 -0
- package/src/db/migrations/lock.ts +269 -0
- package/src/db/migrations/runner.ts +633 -0
- package/src/desktop/__tests__/smoke.test.ts +100 -0
- package/src/desktop/__tests__/window.test.ts +172 -0
- package/src/desktop/__tests__/worker.test.ts +266 -0
- package/src/desktop/index.ts +43 -0
- package/src/desktop/types.ts +158 -0
- package/src/desktop/window.ts +492 -0
- package/src/desktop/worker.ts +180 -0
- package/src/email/__tests__/email.test.ts +355 -0
- package/src/email/index.ts +282 -0
- package/src/email/resend.ts +163 -0
- package/src/email/smtp.ts +64 -0
- package/src/filling/__tests__/session-sqlite.test.ts +454 -0
- package/src/filling/context.ts +72 -78
- package/src/filling/cookie-codec.ts +299 -0
- package/src/filling/deps.ts +25 -1
- package/src/filling/filling.ts +28 -3
- package/src/filling/session-sqlite.ts +617 -0
- package/src/filling/session.ts +265 -216
- package/src/guard/decision-memory.test.ts +52 -22
- package/src/id/__tests__/id.test.ts +120 -0
- package/src/id/index.ts +105 -0
- package/src/kitchen/index.ts +2 -2
- package/src/kitchen/kitchen-handler.ts +86 -0
- package/src/kitchen/stream/activity-sse.ts +2 -1
- package/src/middleware/csrf.ts +328 -0
- package/src/middleware/index.ts +40 -0
- package/src/middleware/oauth/__tests__/oauth.test.ts +574 -0
- package/src/middleware/oauth/index.ts +505 -0
- package/src/middleware/oauth/providers.ts +115 -0
- package/src/middleware/rate-limit/__tests__/rate-limit.test.ts +642 -0
- package/src/middleware/rate-limit/index.ts +522 -0
- package/src/middleware/rate-limit/sqlite-store.ts +382 -0
- package/src/middleware/secure/__tests__/secure.test.ts +360 -0
- package/src/middleware/secure/csp.ts +193 -0
- package/src/middleware/secure/index.ts +417 -0
- package/src/middleware/session.ts +174 -0
- package/src/observability/event-bus.ts +81 -79
- package/src/paths.ts +37 -0
- package/src/perf/hmr-markers.ts +215 -0
- package/src/perf/index.ts +104 -0
- package/src/resource/__tests__/generator.test.ts +603 -2
- package/src/resource/ddl/__tests__/diff.test.ts +639 -0
- package/src/resource/ddl/__tests__/emit.test.ts +799 -0
- package/src/resource/ddl/__tests__/snapshot.test.ts +499 -0
- package/src/resource/ddl/diff.ts +392 -0
- package/src/resource/ddl/emit.ts +548 -0
- package/src/resource/ddl/persistence-types.ts +218 -0
- package/src/resource/ddl/snapshot.ts +447 -0
- package/src/resource/ddl/type-map.ts +223 -0
- package/src/resource/ddl/types.ts +232 -0
- package/src/resource/generator-repo.ts +610 -0
- package/src/resource/generator-schema.ts +476 -0
- package/src/resource/generator.ts +117 -1
- package/src/resource/index.ts +17 -1
- package/src/resource/schema.ts +30 -0
- package/src/router/fs-scanner.ts +3 -0
- package/src/runtime/__tests__/error-boundary-redaction.test.ts +141 -0
- package/src/runtime/__tests__/hdr-client.test.ts +223 -0
- package/src/runtime/__tests__/http-errors.test.ts +117 -0
- package/src/runtime/__tests__/not-found.test.ts +152 -0
- package/src/runtime/boundary.tsx +21 -1
- package/src/runtime/fast-refresh-runtime.ts +322 -0
- package/src/runtime/fast-refresh-types.ts +128 -0
- package/src/runtime/hmr-client.ts +409 -0
- package/src/runtime/http-errors.ts +113 -0
- package/src/runtime/index.ts +6 -0
- package/src/runtime/logger.ts +678 -677
- package/src/runtime/not-found.ts +93 -0
- package/src/runtime/redirect.ts +133 -0
- package/src/runtime/server.ts +518 -20
- package/src/runtime/ssr.ts +340 -10
- package/src/runtime/streaming-ssr.ts +222 -19
- package/src/scheduler/__tests__/scheduler.test.ts +514 -0
- package/src/scheduler/index.ts +343 -0
- package/src/storage/s3/__tests__/s3.test.ts +479 -0
- package/src/storage/s3/index.ts +412 -0
- package/src/testing/index.ts +58 -0
package/src/client/router.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import type { ReactNode } from "react";
|
|
7
|
+
import { startTransition } from "react";
|
|
7
8
|
import {
|
|
8
9
|
getManduData,
|
|
9
10
|
getManduRoute,
|
|
@@ -36,11 +37,11 @@ export interface RouterState {
|
|
|
36
37
|
navigation: NavigationState;
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
export interface ActionResult {
|
|
40
|
-
ok: boolean;
|
|
41
|
-
actionData?: unknown;
|
|
42
|
-
loaderData?: unknown;
|
|
43
|
-
}
|
|
40
|
+
export interface ActionResult {
|
|
41
|
+
ok: boolean;
|
|
42
|
+
actionData?: unknown;
|
|
43
|
+
loaderData?: unknown;
|
|
44
|
+
}
|
|
44
45
|
|
|
45
46
|
export interface NavigateOptions {
|
|
46
47
|
/** history.replaceState 사용 여부 */
|
|
@@ -224,7 +225,7 @@ let activeNavigationController: AbortController | null = null;
|
|
|
224
225
|
/**
|
|
225
226
|
* 페이지 네비게이션
|
|
226
227
|
*/
|
|
227
|
-
export async function navigate(
|
|
228
|
+
export async function navigate(
|
|
228
229
|
to: string,
|
|
229
230
|
options: NavigateOptions = {}
|
|
230
231
|
): Promise<void> {
|
|
@@ -251,36 +252,36 @@ export async function navigate(
|
|
|
251
252
|
// shouldRevalidate 체크 — false면 fetch 없이 URL만 변경
|
|
252
253
|
if (!skipRevalidation && globalShouldRevalidate) {
|
|
253
254
|
const currentUrl = new URL(window.location.href);
|
|
254
|
-
const shouldFetch = globalShouldRevalidate({
|
|
255
|
-
currentUrl,
|
|
256
|
-
nextUrl: url,
|
|
257
|
-
defaultShouldRevalidate: true,
|
|
258
|
-
});
|
|
259
|
-
if (!shouldFetch) {
|
|
260
|
-
const currentState = getRouterStateInternal();
|
|
261
|
-
const nextRoute = currentState.currentRoute
|
|
262
|
-
? {
|
|
263
|
-
...currentState.currentRoute,
|
|
264
|
-
params: extractParamsFromPath(currentState.currentRoute.pattern, url.pathname),
|
|
265
|
-
}
|
|
266
|
-
: null;
|
|
267
|
-
const historyState = nextRoute
|
|
268
|
-
? { routeId: nextRoute.id, params: nextRoute.params }
|
|
269
|
-
: null;
|
|
270
|
-
if (replace) {
|
|
271
|
-
history.replaceState(historyState, "", to);
|
|
272
|
-
} else {
|
|
273
|
-
history.pushState(historyState, "", to);
|
|
274
|
-
}
|
|
275
|
-
setRouterStateInternal({
|
|
276
|
-
...currentState,
|
|
277
|
-
currentRoute: nextRoute,
|
|
278
|
-
actionData: undefined,
|
|
279
|
-
navigation: { state: "idle" },
|
|
280
|
-
});
|
|
281
|
-
notifyListeners();
|
|
282
|
-
if (scroll) window.scrollTo(0, 0);
|
|
283
|
-
return;
|
|
255
|
+
const shouldFetch = globalShouldRevalidate({
|
|
256
|
+
currentUrl,
|
|
257
|
+
nextUrl: url,
|
|
258
|
+
defaultShouldRevalidate: true,
|
|
259
|
+
});
|
|
260
|
+
if (!shouldFetch) {
|
|
261
|
+
const currentState = getRouterStateInternal();
|
|
262
|
+
const nextRoute = currentState.currentRoute
|
|
263
|
+
? {
|
|
264
|
+
...currentState.currentRoute,
|
|
265
|
+
params: extractParamsFromPath(currentState.currentRoute.pattern, url.pathname),
|
|
266
|
+
}
|
|
267
|
+
: null;
|
|
268
|
+
const historyState = nextRoute
|
|
269
|
+
? { routeId: nextRoute.id, params: nextRoute.params }
|
|
270
|
+
: null;
|
|
271
|
+
if (replace) {
|
|
272
|
+
history.replaceState(historyState, "", to);
|
|
273
|
+
} else {
|
|
274
|
+
history.pushState(historyState, "", to);
|
|
275
|
+
}
|
|
276
|
+
setRouterStateInternal({
|
|
277
|
+
...currentState,
|
|
278
|
+
currentRoute: nextRoute,
|
|
279
|
+
actionData: undefined,
|
|
280
|
+
navigation: { state: "idle" },
|
|
281
|
+
});
|
|
282
|
+
notifyListeners();
|
|
283
|
+
if (scroll) window.scrollTo(0, 0);
|
|
284
|
+
return;
|
|
284
285
|
}
|
|
285
286
|
}
|
|
286
287
|
|
|
@@ -565,34 +566,34 @@ export async function submitAction(
|
|
|
565
566
|
if (controller.signal.aborted) return { ok: false };
|
|
566
567
|
|
|
567
568
|
// Revalidation: 서버가 loader를 재실행해서 fresh data를 보내줬으면 갱신
|
|
568
|
-
const nextActionData = result._revalidated ? result.actionData : result;
|
|
569
|
-
|
|
570
|
-
if (result._revalidated && result.loaderData !== undefined) {
|
|
571
|
-
setRouterStateInternal({
|
|
572
|
-
...getRouterStateInternal(),
|
|
573
|
-
loaderData: result.loaderData,
|
|
574
|
-
actionData: nextActionData,
|
|
575
|
-
navigation: { state: "idle" },
|
|
576
|
-
});
|
|
569
|
+
const nextActionData = result._revalidated ? result.actionData : result;
|
|
570
|
+
|
|
571
|
+
if (result._revalidated && result.loaderData !== undefined) {
|
|
572
|
+
setRouterStateInternal({
|
|
573
|
+
...getRouterStateInternal(),
|
|
574
|
+
loaderData: result.loaderData,
|
|
575
|
+
actionData: nextActionData,
|
|
576
|
+
navigation: { state: "idle" },
|
|
577
|
+
});
|
|
577
578
|
|
|
578
579
|
const currentRoute = getRouterStateInternal().currentRoute;
|
|
579
580
|
if (currentRoute) {
|
|
580
581
|
setServerData(currentRoute.id, result.loaderData);
|
|
581
582
|
}
|
|
582
583
|
} else {
|
|
583
|
-
setRouterStateInternal({
|
|
584
|
-
...getRouterStateInternal(),
|
|
585
|
-
actionData: nextActionData,
|
|
586
|
-
navigation: { state: "idle" },
|
|
587
|
-
});
|
|
584
|
+
setRouterStateInternal({
|
|
585
|
+
...getRouterStateInternal(),
|
|
586
|
+
actionData: nextActionData,
|
|
587
|
+
navigation: { state: "idle" },
|
|
588
|
+
});
|
|
588
589
|
}
|
|
589
590
|
|
|
590
|
-
notifyListeners();
|
|
591
|
-
return {
|
|
592
|
-
ok: response.ok,
|
|
593
|
-
actionData: nextActionData,
|
|
594
|
-
loaderData: result._revalidated ? result.loaderData as unknown : undefined,
|
|
595
|
-
};
|
|
591
|
+
notifyListeners();
|
|
592
|
+
return {
|
|
593
|
+
ok: response.ok,
|
|
594
|
+
actionData: nextActionData,
|
|
595
|
+
loaderData: result._revalidated ? result.loaderData as unknown : undefined,
|
|
596
|
+
};
|
|
596
597
|
} catch (error) {
|
|
597
598
|
if (controller.signal.aborted) return { ok: false };
|
|
598
599
|
|
|
@@ -620,6 +621,137 @@ export function getActionData<T = unknown>(): T | undefined {
|
|
|
620
621
|
|
|
621
622
|
let initialized = false;
|
|
622
623
|
|
|
624
|
+
/**
|
|
625
|
+
* Phase 7.3 L-01 — HDR loader payload shape predicate.
|
|
626
|
+
*
|
|
627
|
+
* Exported as an escape hatch for unit tests. Accepts only:
|
|
628
|
+
* - `null` (loader explicitly returned "no data" — a valid outcome).
|
|
629
|
+
* - plain `object` (the typical loader return value shape).
|
|
630
|
+
* Rejects:
|
|
631
|
+
* - `undefined` (we require the server contract to emit `null` explicitly
|
|
632
|
+
* when there is no data, mirroring `Response.json` behaviour).
|
|
633
|
+
* - primitives (`string`, `number`, `boolean`, `symbol`, `bigint`).
|
|
634
|
+
* - `Array` — React loader data is always a bag-of-props, never a bare
|
|
635
|
+
* array. An array payload signals the server contract changed under us.
|
|
636
|
+
*
|
|
637
|
+
* This is DEFENSE-IN-DEPTH: the HDR response is always same-origin, so an
|
|
638
|
+
* attacker cannot inject here without already controlling the server. The
|
|
639
|
+
* predicate catches the real-world failure mode: a skew between server +
|
|
640
|
+
* client versions during a rolling deploy (cached old client fetches new
|
|
641
|
+
* `_data=1` response with a reshape), or a loader that drifted to return
|
|
642
|
+
* a primitive by mistake.
|
|
643
|
+
*
|
|
644
|
+
* We DO allow unknown object keys — loaders legitimately return arbitrary
|
|
645
|
+
* shapes. We only check the outer container.
|
|
646
|
+
*
|
|
647
|
+
* @internal exposed for `__tests__/hdr-l-fixes.test.ts`
|
|
648
|
+
*/
|
|
649
|
+
export function isValidHDRLoaderData(value: unknown): value is object | null {
|
|
650
|
+
if (value === null) return true;
|
|
651
|
+
if (typeof value !== "object") return false;
|
|
652
|
+
// Arrays are `typeof === "object"` — reject them explicitly.
|
|
653
|
+
if (Array.isArray(value)) return false;
|
|
654
|
+
return true;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
/**
|
|
658
|
+
* Phase 7.2 — HDR (Hot Data Revalidation) hook.
|
|
659
|
+
*
|
|
660
|
+
* The dev-time HMR client script (see `bundler/dev.ts`
|
|
661
|
+
* `generateHMRClientScript`) calls this when a `.slot.ts` file
|
|
662
|
+
* changes for the currently-rendered route. We update the router's
|
|
663
|
+
* loader data inside `React.startTransition` so the component tree
|
|
664
|
+
* re-renders with new props while form inputs, scroll position, and
|
|
665
|
+
* focused elements all survive.
|
|
666
|
+
*
|
|
667
|
+
* The global is installed by `initializeRouter()` so dev and prod
|
|
668
|
+
* share the same boot path; in prod builds the HMR script is not
|
|
669
|
+
* emitted and the global is simply never called.
|
|
670
|
+
*
|
|
671
|
+
* Exposed on `window` because the HMR client script (emitted as a
|
|
672
|
+
* raw string) has no module system. Typed via `window-state.ts` if
|
|
673
|
+
* we ever lift the typing, but inline here for now.
|
|
674
|
+
*
|
|
675
|
+
* Phase 7.3 L-01: the incoming `loaderData` is schema-checked via
|
|
676
|
+
* `isValidHDRLoaderData` before we commit it to router state. This is
|
|
677
|
+
* defense-in-depth — the HDR response is same-origin and already
|
|
678
|
+
* trusted, but a version skew between server + client (cached stale
|
|
679
|
+
* client fetching a reshape loader) would otherwise crash React with
|
|
680
|
+
* an unhelpful `undefined.x` inside a fiber. On rejection we log a
|
|
681
|
+
* descriptive warning and drop the update; the next navigation or
|
|
682
|
+
* full reload will resynchronize.
|
|
683
|
+
*/
|
|
684
|
+
function applyHDRUpdate(routeId: string, loaderData: unknown): void {
|
|
685
|
+
const current = getRouterStateInternal();
|
|
686
|
+
const route = current.currentRoute;
|
|
687
|
+
if (!route || route.id !== routeId) {
|
|
688
|
+
// Route mismatch — the user navigated away between the
|
|
689
|
+
// slot-refetch broadcast and the fetch response. Safe to drop.
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
// Phase 7.3 L-01: validate the loaderData shape. On failure keep the
|
|
693
|
+
// existing `loaderData` and warn so the developer notices the
|
|
694
|
+
// contract drift immediately (instead of a cryptic React crash).
|
|
695
|
+
if (!isValidHDRLoaderData(loaderData)) {
|
|
696
|
+
// eslint-disable-next-line no-console
|
|
697
|
+
console.warn(
|
|
698
|
+
"[Mandu HDR] applyHDRUpdate rejected a loader payload that was not a plain object or null. " +
|
|
699
|
+
"This usually signals a server/client version skew — a full reload will resynchronize. " +
|
|
700
|
+
`Received: ${Array.isArray(loaderData) ? "array" : typeof loaderData}`,
|
|
701
|
+
);
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
const nextState: RouterState = {
|
|
705
|
+
currentRoute: route,
|
|
706
|
+
loaderData,
|
|
707
|
+
actionData: current.actionData,
|
|
708
|
+
navigation: current.navigation,
|
|
709
|
+
};
|
|
710
|
+
// startTransition wraps the update so React can interrupt it if
|
|
711
|
+
// more urgent updates come in, and (crucially for HDR) does NOT
|
|
712
|
+
// trigger the "input's state is inconsistent" tearing that a plain
|
|
713
|
+
// setState would. React 19 exports startTransition as a top-level
|
|
714
|
+
// API so it's always available when the router is on the page.
|
|
715
|
+
const apply = () => {
|
|
716
|
+
setRouterStateInternal(nextState);
|
|
717
|
+
setServerData(routeId, loaderData);
|
|
718
|
+
notifyListeners();
|
|
719
|
+
};
|
|
720
|
+
try {
|
|
721
|
+
startTransition(apply);
|
|
722
|
+
} catch {
|
|
723
|
+
// Defensive: if for any reason startTransition throws (e.g. a
|
|
724
|
+
// non-React 19 build ends up with the router on the page), fall
|
|
725
|
+
// back to a direct apply. Prop changes still propagate; only
|
|
726
|
+
// the tearing protection is lost.
|
|
727
|
+
apply();
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* Phase 7.3 L-03 — dev-only detection for the HDR revalidate hook.
|
|
733
|
+
*
|
|
734
|
+
* The router module is bundled into both dev and prod client bundles.
|
|
735
|
+
* We want `window.__MANDU_ROUTER_REVALIDATE__` installed ONLY in dev,
|
|
736
|
+
* because in prod no HMR client script is present to call it — so the
|
|
737
|
+
* global is pure attack surface (an XSS payload could hijack it to
|
|
738
|
+
* coerce the router into rendering arbitrary loader shapes).
|
|
739
|
+
*
|
|
740
|
+
* Mandu's bundler (`packages/core/src/bundler/build.ts`) injects
|
|
741
|
+
* `define: { "process.env.NODE_ENV": JSON.stringify(NODE_ENV) }` into
|
|
742
|
+
* every client bundle, so `process.env.NODE_ENV` is a compile-time
|
|
743
|
+
* constant in the browser. When `NODE_ENV === "production"` the `if`
|
|
744
|
+
* body below is dead-code-eliminated by Bun's minifier.
|
|
745
|
+
*
|
|
746
|
+
* Tested in `__tests__/hdr-l-fixes.test.ts` against both mocked envs.
|
|
747
|
+
*/
|
|
748
|
+
function shouldInstallHDRHook(): boolean {
|
|
749
|
+
// typeof-guard so tests that run without a `process` global (pure
|
|
750
|
+
// happy-dom) don't blow up.
|
|
751
|
+
if (typeof process === "undefined") return true;
|
|
752
|
+
return process.env?.NODE_ENV !== "production";
|
|
753
|
+
}
|
|
754
|
+
|
|
623
755
|
/**
|
|
624
756
|
* 라우터 초기화
|
|
625
757
|
*/
|
|
@@ -637,6 +769,18 @@ export function initializeRouter(): void {
|
|
|
637
769
|
// 링크 클릭 이벤트 위임
|
|
638
770
|
document.addEventListener("click", handleLinkClick);
|
|
639
771
|
|
|
772
|
+
// Phase 7.3 L-03 — expose HDR revalidate hook ONLY in dev builds.
|
|
773
|
+
// The prod client bundle has `process.env.NODE_ENV === "production"`
|
|
774
|
+
// inlined at build time (see `bundler/build.ts` define option), so
|
|
775
|
+
// the entire install block is eliminated from production output.
|
|
776
|
+
// This closes the XSS amplification vector described in the Phase
|
|
777
|
+
// 7.2 security audit (docs/security/phase-7-2-audit.md L-03).
|
|
778
|
+
if (shouldInstallHDRHook()) {
|
|
779
|
+
(window as unknown as {
|
|
780
|
+
__MANDU_ROUTER_REVALIDATE__?: (routeId: string, loaderData: unknown) => void;
|
|
781
|
+
}).__MANDU_ROUTER_REVALIDATE__ = applyHDRUpdate;
|
|
782
|
+
}
|
|
783
|
+
|
|
640
784
|
console.log("[Mandu Router] Initialized");
|
|
641
785
|
}
|
|
642
786
|
|
|
@@ -660,6 +804,15 @@ export function cleanupRouter(): void {
|
|
|
660
804
|
document.removeEventListener("click", handleLinkClick);
|
|
661
805
|
listeners.current.clear();
|
|
662
806
|
initialized = false;
|
|
807
|
+
// Phase 7.3 L-03: also remove the HDR hook so subsequent
|
|
808
|
+
// re-initialization after a teardown starts from a clean slate.
|
|
809
|
+
if (typeof window !== "undefined") {
|
|
810
|
+
try {
|
|
811
|
+
delete (window as unknown as { __MANDU_ROUTER_REVALIDATE__?: unknown }).__MANDU_ROUTER_REVALIDATE__;
|
|
812
|
+
} catch {
|
|
813
|
+
// some browsers refuse `delete` on globals; best-effort is fine.
|
|
814
|
+
}
|
|
815
|
+
}
|
|
663
816
|
}
|
|
664
817
|
|
|
665
818
|
// 자동 초기화 (DOM 준비 시)
|
|
@@ -670,3 +823,8 @@ if (typeof window !== "undefined") {
|
|
|
670
823
|
initializeRouter();
|
|
671
824
|
}
|
|
672
825
|
}
|
|
826
|
+
|
|
827
|
+
// Phase 7.3 L-01: export `applyHDRUpdate` itself for unit tests so we
|
|
828
|
+
// can exercise the schema-check path directly without round-tripping
|
|
829
|
+
// through `window.__MANDU_ROUTER_REVALIDATE__`.
|
|
830
|
+
export { applyHDRUpdate as _testOnly_applyHDRUpdate };
|