@mandujs/core 0.7.3 → 0.7.4

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mandujs/core",
3
- "version": "0.7.3",
3
+ "version": "0.7.4",
4
4
  "description": "Mandu Framework Core - Spec, Generator, Guard, Runtime",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -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 = islandRegistry.get(id);
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
- hydratedRoots.set(id, root);
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
- export { islandRegistry, hydratedRoots };
208
+ // 글로벌 레지스트리 접근용 getter
209
+ export const getIslandRegistry = () => window.__MANDU_ISLANDS__;
210
+ export const getHydratedRoots = () => window.__MANDU_ROOTS__;
210
211
  `;
211
212
  }
212
213
 
@@ -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
- return executeLifecycle(lifecycleWithDefaults, ctx, async () => handler(ctx), options);
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 {