@mandujs/core 0.7.3 → 0.7.5
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 +7 -6
- package/src/client/runtime.ts +22 -14
- package/src/filling/filling.ts +36 -1
package/package.json
CHANGED
package/src/bundler/build.ts
CHANGED
|
@@ -56,12 +56,11 @@ function generateRuntimeSource(): string {
|
|
|
56
56
|
*/
|
|
57
57
|
|
|
58
58
|
// 글로벌 레지스트리 사용 (Island 번들과 공유)
|
|
59
|
+
// 주의: 변수로 캐싱하면 번들러가 인라인 시 new Map()으로 대체함
|
|
60
|
+
// 항상 window.__MANDU_ISLANDS__를 직접 참조해야 함
|
|
59
61
|
window.__MANDU_ISLANDS__ = window.__MANDU_ISLANDS__ || new Map();
|
|
60
62
|
window.__MANDU_ROOTS__ = window.__MANDU_ROOTS__ || new Map();
|
|
61
63
|
|
|
62
|
-
const islandRegistry = window.__MANDU_ISLANDS__;
|
|
63
|
-
const hydratedRoots = window.__MANDU_ROOTS__;
|
|
64
|
-
|
|
65
64
|
// 서버 데이터
|
|
66
65
|
const serverData = window.__MANDU_DATA__ || {};
|
|
67
66
|
|
|
@@ -130,7 +129,7 @@ function scheduleHydration(element, id, priority, data) {
|
|
|
130
129
|
* SSR 플레이스홀더를 Island 컴포넌트로 교체
|
|
131
130
|
*/
|
|
132
131
|
async function hydrateIsland(element, id, data) {
|
|
133
|
-
const loader =
|
|
132
|
+
const loader = window.__MANDU_ISLANDS__.get(id);
|
|
134
133
|
if (!loader) {
|
|
135
134
|
console.warn('[Mandu] Island not found:', id);
|
|
136
135
|
return;
|
|
@@ -159,7 +158,7 @@ async function hydrateIsland(element, id, data) {
|
|
|
159
158
|
// hydrateRoot 대신 createRoot 사용: Island는 SSR과 다른 컨텐츠를 렌더링할 수 있음
|
|
160
159
|
const root = createRoot(element);
|
|
161
160
|
root.render(React.createElement(IslandComponent));
|
|
162
|
-
|
|
161
|
+
window.__MANDU_ROOTS__.set(id, root);
|
|
163
162
|
|
|
164
163
|
// 완료 표시
|
|
165
164
|
element.setAttribute('data-mandu-hydrated', 'true');
|
|
@@ -206,7 +205,9 @@ if (document.readyState === 'loading') {
|
|
|
206
205
|
hydrateIslands();
|
|
207
206
|
}
|
|
208
207
|
|
|
209
|
-
|
|
208
|
+
// 글로벌 레지스트리 접근용 getter
|
|
209
|
+
export const getIslandRegistry = () => window.__MANDU_ISLANDS__;
|
|
210
|
+
export const getHydratedRoots = () => window.__MANDU_ROOTS__;
|
|
210
211
|
`;
|
|
211
212
|
}
|
|
212
213
|
|
package/src/client/runtime.ts
CHANGED
|
@@ -15,14 +15,22 @@ import React from "react";
|
|
|
15
15
|
export type IslandLoader = () => Promise<CompiledIsland<any, any>> | CompiledIsland<any, any>;
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
|
-
* Island 레지스트리
|
|
18
|
+
* Island 레지스트리 (글로벌)
|
|
19
|
+
* 주의: 로컬 Map 사용 시 번들러 인라인 문제로 별도 인스턴스 생성됨
|
|
20
|
+
* 항상 window.__MANDU_ISLANDS__를 직접 참조해야 함
|
|
19
21
|
*/
|
|
20
|
-
|
|
22
|
+
declare global {
|
|
23
|
+
interface Window {
|
|
24
|
+
__MANDU_ISLANDS__: Map<string, IslandLoader>;
|
|
25
|
+
__MANDU_ROOTS__: Map<string, Root>;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
21
28
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
29
|
+
// 글로벌 레지스트리 초기화
|
|
30
|
+
if (typeof window !== "undefined") {
|
|
31
|
+
window.__MANDU_ISLANDS__ = window.__MANDU_ISLANDS__ || new Map<string, IslandLoader>();
|
|
32
|
+
window.__MANDU_ROOTS__ = window.__MANDU_ROOTS__ || new Map<string, Root>();
|
|
33
|
+
}
|
|
26
34
|
|
|
27
35
|
/**
|
|
28
36
|
* Hydration 상태 추적
|
|
@@ -45,14 +53,14 @@ const hydrationState: HydrationState = {
|
|
|
45
53
|
* Island 등록
|
|
46
54
|
*/
|
|
47
55
|
export function registerIsland(id: string, loader: IslandLoader): void {
|
|
48
|
-
|
|
56
|
+
window.__MANDU_ISLANDS__.set(id, loader);
|
|
49
57
|
}
|
|
50
58
|
|
|
51
59
|
/**
|
|
52
60
|
* 등록된 모든 Island 가져오기
|
|
53
61
|
*/
|
|
54
62
|
export function getRegisteredIslands(): string[] {
|
|
55
|
-
return Array.from(
|
|
63
|
+
return Array.from(window.__MANDU_ISLANDS__.keys());
|
|
56
64
|
}
|
|
57
65
|
|
|
58
66
|
/**
|
|
@@ -139,7 +147,7 @@ async function hydrateIsland(
|
|
|
139
147
|
id: string,
|
|
140
148
|
serverData: unknown
|
|
141
149
|
): Promise<void> {
|
|
142
|
-
const loader =
|
|
150
|
+
const loader = window.__MANDU_ISLANDS__.get(id);
|
|
143
151
|
if (!loader) {
|
|
144
152
|
console.warn(`[Mandu] Island not registered: ${id}`);
|
|
145
153
|
hydrationState.failed++;
|
|
@@ -179,7 +187,7 @@ async function hydrateIsland(
|
|
|
179
187
|
|
|
180
188
|
// Hydrate
|
|
181
189
|
const root = hydrateRoot(element, content);
|
|
182
|
-
|
|
190
|
+
window.__MANDU_ROOTS__.set(id, root);
|
|
183
191
|
|
|
184
192
|
// 상태 업데이트
|
|
185
193
|
element.setAttribute("data-mandu-hydrated", "true");
|
|
@@ -252,13 +260,13 @@ export function getHydrationState(): Readonly<HydrationState> {
|
|
|
252
260
|
* 특정 Island unmount
|
|
253
261
|
*/
|
|
254
262
|
export function unmountIsland(id: string): boolean {
|
|
255
|
-
const root =
|
|
263
|
+
const root = window.__MANDU_ROOTS__.get(id);
|
|
256
264
|
if (!root) {
|
|
257
265
|
return false;
|
|
258
266
|
}
|
|
259
267
|
|
|
260
268
|
root.unmount();
|
|
261
|
-
|
|
269
|
+
window.__MANDU_ROOTS__.delete(id);
|
|
262
270
|
return true;
|
|
263
271
|
}
|
|
264
272
|
|
|
@@ -266,9 +274,9 @@ export function unmountIsland(id: string): boolean {
|
|
|
266
274
|
* 모든 Island unmount
|
|
267
275
|
*/
|
|
268
276
|
export function unmountAllIslands(): void {
|
|
269
|
-
for (const [id, root] of
|
|
277
|
+
for (const [id, root] of window.__MANDU_ROOTS__) {
|
|
270
278
|
root.unmount();
|
|
271
|
-
|
|
279
|
+
window.__MANDU_ROOTS__.delete(id);
|
|
272
280
|
}
|
|
273
281
|
}
|
|
274
282
|
|
package/src/filling/filling.ts
CHANGED
|
@@ -7,6 +7,11 @@ import { ManduContext, ValidationError } from "./context";
|
|
|
7
7
|
import { AuthenticationError, AuthorizationError } from "./auth";
|
|
8
8
|
import { ErrorClassifier, formatErrorResponse, ErrorCode } from "../error";
|
|
9
9
|
import { createContract, type ContractDefinition, type ContractInstance } from "../contract";
|
|
10
|
+
import {
|
|
11
|
+
type Middleware as RuntimeMiddleware,
|
|
12
|
+
type MiddlewareEntry,
|
|
13
|
+
compose,
|
|
14
|
+
} from "../runtime/compose";
|
|
10
15
|
import {
|
|
11
16
|
type LifecycleStore,
|
|
12
17
|
type OnRequestHandler,
|
|
@@ -53,12 +58,14 @@ interface FillingConfig<TLoaderData = unknown> {
|
|
|
53
58
|
handlers: Map<HttpMethod, Handler>;
|
|
54
59
|
loader?: Loader<TLoaderData>;
|
|
55
60
|
lifecycle: LifecycleStore;
|
|
61
|
+
middleware: MiddlewareEntry[];
|
|
56
62
|
}
|
|
57
63
|
|
|
58
64
|
export class ManduFilling<TLoaderData = unknown> {
|
|
59
65
|
private config: FillingConfig<TLoaderData> = {
|
|
60
66
|
handlers: new Map(),
|
|
61
67
|
lifecycle: createLifecycleStore(),
|
|
68
|
+
middleware: [],
|
|
62
69
|
};
|
|
63
70
|
|
|
64
71
|
loader(loaderFn: Loader<TLoaderData>): this {
|
|
@@ -139,6 +146,19 @@ export class ManduFilling<TLoaderData = unknown> {
|
|
|
139
146
|
return this;
|
|
140
147
|
}
|
|
141
148
|
|
|
149
|
+
/**
|
|
150
|
+
* Compose-style middleware (Hono/Koa 스타일)
|
|
151
|
+
* lifecycle의 handler 단계에서 실행됨
|
|
152
|
+
*/
|
|
153
|
+
middleware(fn: RuntimeMiddleware, name?: string): this {
|
|
154
|
+
this.config.middleware.push({
|
|
155
|
+
fn,
|
|
156
|
+
name: name || fn.name || `middleware_${this.config.middleware.length}`,
|
|
157
|
+
isAsync: fn.constructor.name === "AsyncFunction",
|
|
158
|
+
});
|
|
159
|
+
return this;
|
|
160
|
+
}
|
|
161
|
+
|
|
142
162
|
onParse(fn: OnParseHandler): this {
|
|
143
163
|
this.config.lifecycle.onParse.push({ fn, scope: "local" });
|
|
144
164
|
return this;
|
|
@@ -197,7 +217,22 @@ export class ManduFilling<TLoaderData = unknown> {
|
|
|
197
217
|
return ctx.json({ status: "error", message: `Method ${method} not allowed`, allowed: Array.from(this.config.handlers.keys()) }, 405);
|
|
198
218
|
}
|
|
199
219
|
const lifecycleWithDefaults = this.createLifecycleWithDefaults(routeContext);
|
|
200
|
-
|
|
220
|
+
const runHandler = async () => {
|
|
221
|
+
if (this.config.middleware.length === 0) {
|
|
222
|
+
return handler(ctx);
|
|
223
|
+
}
|
|
224
|
+
const chain: MiddlewareEntry[] = [
|
|
225
|
+
...this.config.middleware,
|
|
226
|
+
{
|
|
227
|
+
fn: async (innerCtx) => handler(innerCtx),
|
|
228
|
+
name: "handler",
|
|
229
|
+
isAsync: true,
|
|
230
|
+
},
|
|
231
|
+
];
|
|
232
|
+
const composed = compose(chain);
|
|
233
|
+
return composed(ctx);
|
|
234
|
+
};
|
|
235
|
+
return executeLifecycle(lifecycleWithDefaults, ctx, runHandler, options);
|
|
201
236
|
}
|
|
202
237
|
|
|
203
238
|
private createLifecycleWithDefaults(routeContext?: { routeId: string; pattern: string }): LifecycleStore {
|