@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,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mandu Middleware Compose π
|
|
3
|
+
* Hono μ€νμΌ λ―Έλ€μ¨μ΄ μ‘°ν© ν¨ν΄
|
|
4
|
+
*
|
|
5
|
+
* @see https://github.com/honojs/hono/blob/main/src/compose.ts
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ManduContext } from "../filling/context";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Next ν¨μ νμ
|
|
12
|
+
*/
|
|
13
|
+
export type Next = () => Promise<void>;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* λ―Έλ€μ¨μ΄ ν¨μ νμ
|
|
17
|
+
* - Response λ°ν: μ²΄μΈ μ€λ¨ (Guard μν )
|
|
18
|
+
* - void λ°ν: λ€μ λ―Έλ€μ¨μ΄ μ€ν
|
|
19
|
+
*/
|
|
20
|
+
export type Middleware = (
|
|
21
|
+
ctx: ManduContext,
|
|
22
|
+
next: Next
|
|
23
|
+
) => Response | void | Promise<Response | void>;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* μλ¬ νΈλ€λ¬ νμ
|
|
27
|
+
*/
|
|
28
|
+
export type ErrorHandler = (
|
|
29
|
+
error: Error,
|
|
30
|
+
ctx: ManduContext
|
|
31
|
+
) => Response | Promise<Response>;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* NotFound νΈλ€λ¬ νμ
|
|
35
|
+
*/
|
|
36
|
+
export type NotFoundHandler = (ctx: ManduContext) => Response | Promise<Response>;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* λ―Έλ€μ¨μ΄ μνΈλ¦¬ (λ©νλ°μ΄ν° ν¬ν¨)
|
|
40
|
+
*/
|
|
41
|
+
export interface MiddlewareEntry {
|
|
42
|
+
fn: Middleware;
|
|
43
|
+
name?: string;
|
|
44
|
+
isAsync?: boolean;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Compose μ΅μ
|
|
49
|
+
*/
|
|
50
|
+
export interface ComposeOptions {
|
|
51
|
+
onError?: ErrorHandler;
|
|
52
|
+
onNotFound?: NotFoundHandler;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* λ―Έλ€μ¨μ΄ ν¨μλ€μ νλμ μ€ν ν¨μλ‘ μ‘°ν©
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```typescript
|
|
60
|
+
* const middleware = [
|
|
61
|
+
* { fn: async (ctx, next) => { console.log('before'); await next(); console.log('after'); } },
|
|
62
|
+
* { fn: async (ctx, next) => { return ctx.ok({ data: 'hello' }); } },
|
|
63
|
+
* ];
|
|
64
|
+
*
|
|
65
|
+
* const handler = compose(middleware, {
|
|
66
|
+
* onError: (err, ctx) => ctx.json({ error: err.message }, 500),
|
|
67
|
+
* onNotFound: (ctx) => ctx.notFound(),
|
|
68
|
+
* });
|
|
69
|
+
*
|
|
70
|
+
* const response = await handler(context);
|
|
71
|
+
* ```
|
|
72
|
+
*/
|
|
73
|
+
export function compose(
|
|
74
|
+
middleware: MiddlewareEntry[],
|
|
75
|
+
options: ComposeOptions = {}
|
|
76
|
+
): (ctx: ManduContext) => Promise<Response> {
|
|
77
|
+
const { onError, onNotFound } = options;
|
|
78
|
+
|
|
79
|
+
return async (ctx: ManduContext): Promise<Response> => {
|
|
80
|
+
let index = -1;
|
|
81
|
+
let finalResponse: Response | undefined;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* λ―Έλ€μ¨μ΄ μμ°¨ μ€ν
|
|
85
|
+
* @param i νμ¬ μΈλ±μ€
|
|
86
|
+
*/
|
|
87
|
+
async function dispatch(i: number): Promise<void> {
|
|
88
|
+
// next() μ΄μ€ νΈμΆ λ°©μ§
|
|
89
|
+
if (i <= index) {
|
|
90
|
+
throw new Error("next() called multiple times");
|
|
91
|
+
}
|
|
92
|
+
index = i;
|
|
93
|
+
|
|
94
|
+
const entry = middleware[i];
|
|
95
|
+
|
|
96
|
+
if (!entry) {
|
|
97
|
+
// λͺ¨λ λ―Έλ€μ¨μ΄ ν΅κ³Ό ν νΈλ€λ¬ μμ
|
|
98
|
+
if (!finalResponse && onNotFound) {
|
|
99
|
+
finalResponse = await onNotFound(ctx);
|
|
100
|
+
}
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
const result = await entry.fn(ctx, () => dispatch(i + 1));
|
|
106
|
+
|
|
107
|
+
// Response λ°ν μ μ²΄μΈ μ€λ¨
|
|
108
|
+
if (result instanceof Response) {
|
|
109
|
+
finalResponse = result;
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
} catch (err) {
|
|
113
|
+
if (err instanceof Error && onError) {
|
|
114
|
+
finalResponse = await onError(err, ctx);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
throw err;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
await dispatch(0);
|
|
122
|
+
|
|
123
|
+
// μλ΅μ΄ μμΌλ©΄ 404
|
|
124
|
+
if (!finalResponse) {
|
|
125
|
+
if (onNotFound) {
|
|
126
|
+
finalResponse = await onNotFound(ctx);
|
|
127
|
+
} else {
|
|
128
|
+
finalResponse = new Response("Not Found", { status: 404 });
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return finalResponse;
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* λ―Έλ€μ¨μ΄ λ°°μ΄ μμ± ν¬νΌ
|
|
138
|
+
*
|
|
139
|
+
* @example
|
|
140
|
+
* ```typescript
|
|
141
|
+
* const mw = createMiddleware([
|
|
142
|
+
* authGuard,
|
|
143
|
+
* rateLimitGuard,
|
|
144
|
+
* mainHandler,
|
|
145
|
+
* ]);
|
|
146
|
+
* ```
|
|
147
|
+
*/
|
|
148
|
+
export function createMiddleware(
|
|
149
|
+
fns: Middleware[]
|
|
150
|
+
): MiddlewareEntry[] {
|
|
151
|
+
return fns.map((fn, i) => ({
|
|
152
|
+
fn,
|
|
153
|
+
name: fn.name || `middleware_${i}`,
|
|
154
|
+
isAsync: fn.constructor.name === "AsyncFunction",
|
|
155
|
+
}));
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* λ―Έλ€μ¨μ΄ μ²΄μΈ λΉλ
|
|
160
|
+
*
|
|
161
|
+
* @example
|
|
162
|
+
* ```typescript
|
|
163
|
+
* const chain = new MiddlewareChain()
|
|
164
|
+
* .use(authGuard)
|
|
165
|
+
* .use(rateLimitGuard)
|
|
166
|
+
* .use(mainHandler)
|
|
167
|
+
* .onError((err, ctx) => ctx.json({ error: err.message }, 500))
|
|
168
|
+
* .build();
|
|
169
|
+
*
|
|
170
|
+
* const response = await chain(ctx);
|
|
171
|
+
* ```
|
|
172
|
+
*/
|
|
173
|
+
export class MiddlewareChain {
|
|
174
|
+
private middleware: MiddlewareEntry[] = [];
|
|
175
|
+
private errorHandler?: ErrorHandler;
|
|
176
|
+
private notFoundHandler?: NotFoundHandler;
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* λ―Έλ€μ¨μ΄ μΆκ°
|
|
180
|
+
*/
|
|
181
|
+
use(fn: Middleware, name?: string): this {
|
|
182
|
+
this.middleware.push({
|
|
183
|
+
fn,
|
|
184
|
+
name: name || fn.name || `middleware_${this.middleware.length}`,
|
|
185
|
+
isAsync: fn.constructor.name === "AsyncFunction",
|
|
186
|
+
});
|
|
187
|
+
return this;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* μλ¬ νΈλ€λ¬ μ€μ
|
|
192
|
+
*/
|
|
193
|
+
onError(handler: ErrorHandler): this {
|
|
194
|
+
this.errorHandler = handler;
|
|
195
|
+
return this;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* NotFound νΈλ€λ¬ μ€μ
|
|
200
|
+
*/
|
|
201
|
+
onNotFound(handler: NotFoundHandler): this {
|
|
202
|
+
this.notFoundHandler = handler;
|
|
203
|
+
return this;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* λ―Έλ€μ¨μ΄ μ²΄μΈ λΉλ
|
|
208
|
+
*/
|
|
209
|
+
build(): (ctx: ManduContext) => Promise<Response> {
|
|
210
|
+
return compose(this.middleware, {
|
|
211
|
+
onError: this.errorHandler,
|
|
212
|
+
onNotFound: this.notFoundHandler,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* λ―Έλ€μ¨μ΄ λͺ©λ‘ μ‘°ν
|
|
218
|
+
*/
|
|
219
|
+
getMiddleware(): MiddlewareEntry[] {
|
|
220
|
+
return [...this.middleware];
|
|
221
|
+
}
|
|
222
|
+
}
|
package/src/runtime/index.ts
CHANGED
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mandu Lifecycle Hooks π
|
|
3
|
+
* Elysia μ€νμΌ λΌμ΄νμ¬μ΄ν΄ ν
체κ³
|
|
4
|
+
*
|
|
5
|
+
* @see https://elysiajs.com/life-cycle/overview.html
|
|
6
|
+
*
|
|
7
|
+
* μμ² νλ¦:
|
|
8
|
+
* 1. onRequest - μμ² μμ
|
|
9
|
+
* 2. onParse - λ°λ νμ± (POST, PUT, PATCH)
|
|
10
|
+
* 3. beforeHandle - νΈλ€λ¬ μ (Guard μν )
|
|
11
|
+
* 4. [Handler] - λ©μΈ νΈλ€λ¬ μ€ν
|
|
12
|
+
* 5. afterHandle - νΈλ€λ¬ ν (μλ΅ λ³ν)
|
|
13
|
+
* 6. mapResponse - μλ΅ λ§€ν
|
|
14
|
+
* 7. afterResponse - μλ΅ ν (λ‘κΉ
, μ 리)
|
|
15
|
+
*
|
|
16
|
+
* μλ¬ λ°μ μ:
|
|
17
|
+
* - onError - μλ¬ νΈλ€λ§
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import type { ManduContext } from "../filling/context";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* ν
μ€μ½ν
|
|
24
|
+
* - global: λͺ¨λ λΌμ°νΈμ μ μ©
|
|
25
|
+
* - scoped: νμ¬ νλ¬κ·ΈμΈ/λΌμ°νΈ κ·Έλ£Ήμ μ μ©
|
|
26
|
+
* - local: νμ¬ λΌμ°νΈμλ§ μ μ©
|
|
27
|
+
*/
|
|
28
|
+
export type HookScope = "global" | "scoped" | "local";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* ν
컨ν
μ΄λ
|
|
32
|
+
*/
|
|
33
|
+
export interface HookContainer<T extends Function = Function> {
|
|
34
|
+
fn: T;
|
|
35
|
+
scope: HookScope;
|
|
36
|
+
name?: string;
|
|
37
|
+
checksum?: number; // μ€λ³΅ μ κ±°μ©
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ============================================
|
|
41
|
+
// ν
νμ
μ μ
|
|
42
|
+
// ============================================
|
|
43
|
+
|
|
44
|
+
/** μμ² μμ ν
*/
|
|
45
|
+
export type OnRequestHandler = (ctx: ManduContext) => void | Promise<void>;
|
|
46
|
+
|
|
47
|
+
/** λ°λ νμ± ν
*/
|
|
48
|
+
export type OnParseHandler = (ctx: ManduContext) => void | Promise<void>;
|
|
49
|
+
|
|
50
|
+
/** νΈλ€λ¬ μ ν
(Guard μν ) - Response λ°ν μ μ²΄μΈ μ€λ¨ */
|
|
51
|
+
export type BeforeHandleHandler = (
|
|
52
|
+
ctx: ManduContext
|
|
53
|
+
) => Response | void | Promise<Response | void>;
|
|
54
|
+
|
|
55
|
+
/** νΈλ€λ¬ ν ν
- μλ΅ λ³ν κ°λ₯ */
|
|
56
|
+
export type AfterHandleHandler = (
|
|
57
|
+
ctx: ManduContext,
|
|
58
|
+
response: Response
|
|
59
|
+
) => Response | Promise<Response>;
|
|
60
|
+
|
|
61
|
+
/** μλ΅ λ§€ν ν
*/
|
|
62
|
+
export type MapResponseHandler = (
|
|
63
|
+
ctx: ManduContext,
|
|
64
|
+
response: Response
|
|
65
|
+
) => Response | Promise<Response>;
|
|
66
|
+
|
|
67
|
+
/** μλ΅ ν ν
(λΉλκΈ°, μλ΅μ μν₯ μμ) */
|
|
68
|
+
export type AfterResponseHandler = (ctx: ManduContext) => void | Promise<void>;
|
|
69
|
+
|
|
70
|
+
/** μλ¬ νΈλ€λ§ ν
- Response λ°ν μ μλ¬ μλ΅μΌλ‘ μ¬μ© */
|
|
71
|
+
export type OnErrorHandler = (
|
|
72
|
+
ctx: ManduContext,
|
|
73
|
+
error: Error
|
|
74
|
+
) => Response | void | Promise<Response | void>;
|
|
75
|
+
|
|
76
|
+
// ============================================
|
|
77
|
+
// λΌμ΄νμ¬μ΄ν΄ μ€ν μ΄
|
|
78
|
+
// ============================================
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* λΌμ΄νμ¬μ΄ν΄ ν
μ€ν μ΄
|
|
82
|
+
*/
|
|
83
|
+
export interface LifecycleStore {
|
|
84
|
+
onRequest: HookContainer<OnRequestHandler>[];
|
|
85
|
+
onParse: HookContainer<OnParseHandler>[];
|
|
86
|
+
beforeHandle: HookContainer<BeforeHandleHandler>[];
|
|
87
|
+
afterHandle: HookContainer<AfterHandleHandler>[];
|
|
88
|
+
mapResponse: HookContainer<MapResponseHandler>[];
|
|
89
|
+
afterResponse: HookContainer<AfterResponseHandler>[];
|
|
90
|
+
onError: HookContainer<OnErrorHandler>[];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* λΉ λΌμ΄νμ¬μ΄ν΄ μ€ν μ΄ μμ±
|
|
95
|
+
*/
|
|
96
|
+
export function createLifecycleStore(): LifecycleStore {
|
|
97
|
+
return {
|
|
98
|
+
onRequest: [],
|
|
99
|
+
onParse: [],
|
|
100
|
+
beforeHandle: [],
|
|
101
|
+
afterHandle: [],
|
|
102
|
+
mapResponse: [],
|
|
103
|
+
afterResponse: [],
|
|
104
|
+
onError: [],
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ============================================
|
|
109
|
+
// λΌμ΄νμ¬μ΄ν΄ μ€ν
|
|
110
|
+
// ============================================
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* λΌμ΄νμ¬μ΄ν΄ μ€ν μ΅μ
|
|
114
|
+
*/
|
|
115
|
+
export interface ExecuteOptions {
|
|
116
|
+
/** λ°λ νμ±μ΄ νμν λ©μλ */
|
|
117
|
+
parseBodyMethods?: string[];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const DEFAULT_PARSE_BODY_METHODS = ["POST", "PUT", "PATCH"];
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* λΌμ΄νμ¬μ΄ν΄ μ€ν
|
|
124
|
+
*
|
|
125
|
+
* @param lifecycle λΌμ΄νμ¬μ΄ν΄ μ€ν μ΄
|
|
126
|
+
* @param ctx ManduContext
|
|
127
|
+
* @param handler λ©μΈ νΈλ€λ¬
|
|
128
|
+
* @param options μ΅μ
|
|
129
|
+
*
|
|
130
|
+
* @example
|
|
131
|
+
* ```typescript
|
|
132
|
+
* const lifecycle = createLifecycleStore();
|
|
133
|
+
* lifecycle.onRequest.push({ fn: (ctx) => console.log('Request started'), scope: 'local' });
|
|
134
|
+
* lifecycle.beforeHandle.push({ fn: authGuard, scope: 'local' });
|
|
135
|
+
*
|
|
136
|
+
* const response = await executeLifecycle(
|
|
137
|
+
* lifecycle,
|
|
138
|
+
* ctx,
|
|
139
|
+
* async () => ctx.ok({ data: 'hello' })
|
|
140
|
+
* );
|
|
141
|
+
* ```
|
|
142
|
+
*/
|
|
143
|
+
export async function executeLifecycle(
|
|
144
|
+
lifecycle: LifecycleStore,
|
|
145
|
+
ctx: ManduContext,
|
|
146
|
+
handler: () => Promise<Response>,
|
|
147
|
+
options: ExecuteOptions = {}
|
|
148
|
+
): Promise<Response> {
|
|
149
|
+
const { parseBodyMethods = DEFAULT_PARSE_BODY_METHODS } = options;
|
|
150
|
+
let response: Response;
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
// 1. onRequest
|
|
154
|
+
for (const hook of lifecycle.onRequest) {
|
|
155
|
+
await hook.fn(ctx);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// 2. onParse (λ°λκ° μλ λ©μλλ§)
|
|
159
|
+
if (parseBodyMethods.includes(ctx.req.method)) {
|
|
160
|
+
for (const hook of lifecycle.onParse) {
|
|
161
|
+
await hook.fn(ctx);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// 3. beforeHandle (Guard μν )
|
|
166
|
+
for (const hook of lifecycle.beforeHandle) {
|
|
167
|
+
const result = await hook.fn(ctx);
|
|
168
|
+
if (result instanceof Response) {
|
|
169
|
+
// Response λ°ν μ μ²΄μΈ μ€λ¨, afterHandle/mapResponse 건λλ
|
|
170
|
+
response = result;
|
|
171
|
+
// afterResponseλ μ€ν
|
|
172
|
+
scheduleAfterResponse(lifecycle.afterResponse, ctx);
|
|
173
|
+
return response;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// 4. λ©μΈ νΈλ€λ¬ μ€ν
|
|
178
|
+
response = await handler();
|
|
179
|
+
|
|
180
|
+
// 5. afterHandle
|
|
181
|
+
for (const hook of lifecycle.afterHandle) {
|
|
182
|
+
response = await hook.fn(ctx, response);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// 6. mapResponse
|
|
186
|
+
for (const hook of lifecycle.mapResponse) {
|
|
187
|
+
response = await hook.fn(ctx, response);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// 7. afterResponse (λΉλκΈ°)
|
|
191
|
+
scheduleAfterResponse(lifecycle.afterResponse, ctx);
|
|
192
|
+
|
|
193
|
+
return response;
|
|
194
|
+
} catch (err) {
|
|
195
|
+
// onError μ²λ¦¬
|
|
196
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
197
|
+
|
|
198
|
+
for (const hook of lifecycle.onError) {
|
|
199
|
+
const result = await hook.fn(ctx, error);
|
|
200
|
+
if (result instanceof Response) {
|
|
201
|
+
// afterResponseλ μλ¬ μμλ μ€ν
|
|
202
|
+
scheduleAfterResponse(lifecycle.afterResponse, ctx);
|
|
203
|
+
return result;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// μλ¬ νΈλ€λ¬κ° Responseλ₯Ό λ°ννμ§ μμΌλ©΄ μ¬throw
|
|
208
|
+
throw error;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* afterResponse ν
λΉλκΈ° μ€ν (μλ΅ ν)
|
|
214
|
+
*/
|
|
215
|
+
function scheduleAfterResponse(
|
|
216
|
+
hooks: HookContainer<AfterResponseHandler>[],
|
|
217
|
+
ctx: ManduContext
|
|
218
|
+
): void {
|
|
219
|
+
if (hooks.length === 0) return;
|
|
220
|
+
|
|
221
|
+
// queueMicrotaskλ‘ μλ΅ ν μ€ν
|
|
222
|
+
queueMicrotask(async () => {
|
|
223
|
+
for (const hook of hooks) {
|
|
224
|
+
try {
|
|
225
|
+
await hook.fn(ctx);
|
|
226
|
+
} catch (err) {
|
|
227
|
+
console.error("[Mandu] afterResponse hook error:", err);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ============================================
|
|
234
|
+
// λΌμ΄νμ¬μ΄ν΄ λΉλ
|
|
235
|
+
// ============================================
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* λΌμ΄νμ¬μ΄ν΄ λΉλ
|
|
239
|
+
*
|
|
240
|
+
* @example
|
|
241
|
+
* ```typescript
|
|
242
|
+
* const lifecycle = new LifecycleBuilder()
|
|
243
|
+
* .onRequest((ctx) => console.log('Request:', ctx.req.url))
|
|
244
|
+
* .beforeHandle(authGuard)
|
|
245
|
+
* .afterHandle((ctx, res) => {
|
|
246
|
+
* // μλ΅ ν€λ μΆκ°
|
|
247
|
+
* res.headers.set('X-Custom', 'value');
|
|
248
|
+
* return res;
|
|
249
|
+
* })
|
|
250
|
+
* .onError((ctx, err) => ctx.json({ error: err.message }, 500))
|
|
251
|
+
* .build();
|
|
252
|
+
* ```
|
|
253
|
+
*/
|
|
254
|
+
export class LifecycleBuilder {
|
|
255
|
+
private store: LifecycleStore = createLifecycleStore();
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* μμ² μμ ν
μΆκ°
|
|
259
|
+
*/
|
|
260
|
+
onRequest(fn: OnRequestHandler, scope: HookScope = "local"): this {
|
|
261
|
+
this.store.onRequest.push({ fn, scope });
|
|
262
|
+
return this;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* λ°λ νμ± ν
μΆκ°
|
|
267
|
+
*/
|
|
268
|
+
onParse(fn: OnParseHandler, scope: HookScope = "local"): this {
|
|
269
|
+
this.store.onParse.push({ fn, scope });
|
|
270
|
+
return this;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* νΈλ€λ¬ μ ν
μΆκ° (Guard μν )
|
|
275
|
+
*/
|
|
276
|
+
beforeHandle(fn: BeforeHandleHandler, scope: HookScope = "local"): this {
|
|
277
|
+
this.store.beforeHandle.push({ fn, scope });
|
|
278
|
+
return this;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* νΈλ€λ¬ ν ν
μΆκ°
|
|
283
|
+
*/
|
|
284
|
+
afterHandle(fn: AfterHandleHandler, scope: HookScope = "local"): this {
|
|
285
|
+
this.store.afterHandle.push({ fn, scope });
|
|
286
|
+
return this;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* μλ΅ λ§€ν ν
μΆκ°
|
|
291
|
+
*/
|
|
292
|
+
mapResponse(fn: MapResponseHandler, scope: HookScope = "local"): this {
|
|
293
|
+
this.store.mapResponse.push({ fn, scope });
|
|
294
|
+
return this;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* μλ΅ ν ν
μΆκ°
|
|
299
|
+
*/
|
|
300
|
+
afterResponse(fn: AfterResponseHandler, scope: HookScope = "local"): this {
|
|
301
|
+
this.store.afterResponse.push({ fn, scope });
|
|
302
|
+
return this;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* μλ¬ νΈλ€λ§ ν
μΆκ°
|
|
307
|
+
*/
|
|
308
|
+
onError(fn: OnErrorHandler, scope: HookScope = "local"): this {
|
|
309
|
+
this.store.onError.push({ fn, scope });
|
|
310
|
+
return this;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* λΌμ΄νμ¬μ΄ν΄ μ€ν μ΄ λΉλ
|
|
315
|
+
*/
|
|
316
|
+
build(): LifecycleStore {
|
|
317
|
+
return { ...this.store };
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* λ€λ₯Έ λΌμ΄νμ¬μ΄ν΄κ³Ό λ³ν©
|
|
322
|
+
*/
|
|
323
|
+
merge(other: LifecycleStore): this {
|
|
324
|
+
this.store.onRequest.push(...other.onRequest);
|
|
325
|
+
this.store.onParse.push(...other.onParse);
|
|
326
|
+
this.store.beforeHandle.push(...other.beforeHandle);
|
|
327
|
+
this.store.afterHandle.push(...other.afterHandle);
|
|
328
|
+
this.store.mapResponse.push(...other.mapResponse);
|
|
329
|
+
this.store.afterResponse.push(...other.afterResponse);
|
|
330
|
+
this.store.onError.push(...other.onError);
|
|
331
|
+
return this;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// ============================================
|
|
336
|
+
// μ νΈλ¦¬ν°
|
|
337
|
+
// ============================================
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* ν
μ€λ³΅ μ κ±° (checksum κΈ°λ°)
|
|
341
|
+
*/
|
|
342
|
+
export function deduplicateHooks<T extends HookContainer>(hooks: T[]): T[] {
|
|
343
|
+
const seen = new Set<number>();
|
|
344
|
+
return hooks.filter((hook) => {
|
|
345
|
+
if (hook.checksum === undefined) return true;
|
|
346
|
+
if (seen.has(hook.checksum)) return false;
|
|
347
|
+
seen.add(hook.checksum);
|
|
348
|
+
return true;
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* μ€μ½νλ³ ν
νν°λ§
|
|
354
|
+
*/
|
|
355
|
+
export function filterHooksByScope<T extends HookContainer>(
|
|
356
|
+
hooks: T[],
|
|
357
|
+
scopes: HookScope[]
|
|
358
|
+
): T[] {
|
|
359
|
+
return hooks.filter((hook) => scopes.includes(hook.scope));
|
|
360
|
+
}
|
package/src/runtime/server.ts
CHANGED
|
@@ -315,6 +315,9 @@ async function handleRequest(req: Request, router: Router): Promise<Response> {
|
|
|
315
315
|
let loaderData: unknown;
|
|
316
316
|
let component: RouteComponent | undefined;
|
|
317
317
|
|
|
318
|
+
// Client-side Routing: λ°μ΄ν° μμ² κ°μ§
|
|
319
|
+
const isDataRequest = url.searchParams.has("_data");
|
|
320
|
+
|
|
318
321
|
// 1. PageHandler λ°©μ (μ κ· - filling ν¬ν¨)
|
|
319
322
|
const pageHandler = pageHandlers.get(route.id);
|
|
320
323
|
if (pageHandler) {
|
|
@@ -363,6 +366,18 @@ async function handleRequest(req: Request, router: Router): Promise<Response> {
|
|
|
363
366
|
}
|
|
364
367
|
}
|
|
365
368
|
|
|
369
|
+
// Client-side Routing: λ°μ΄ν°λ§ λ°ν (JSON)
|
|
370
|
+
if (isDataRequest) {
|
|
371
|
+
return Response.json({
|
|
372
|
+
routeId: route.id,
|
|
373
|
+
pattern: route.pattern,
|
|
374
|
+
params,
|
|
375
|
+
loaderData: loaderData ?? null,
|
|
376
|
+
timestamp: Date.now(),
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// SSR λ λλ§ (κΈ°μ‘΄ λ‘μ§)
|
|
366
381
|
const appCreator = createAppFn || defaultCreateApp;
|
|
367
382
|
try {
|
|
368
383
|
const app = appCreator({
|
|
@@ -385,6 +400,9 @@ async function handleRequest(req: Request, router: Router): Promise<Response> {
|
|
|
385
400
|
hydration: route.hydration,
|
|
386
401
|
bundleManifest: serverSettings.bundleManifest,
|
|
387
402
|
serverData,
|
|
403
|
+
// Client-side Routing νμ±ν μ 보 μ λ¬
|
|
404
|
+
enableClientRouter: true,
|
|
405
|
+
routePattern: route.pattern,
|
|
388
406
|
});
|
|
389
407
|
} catch (err) {
|
|
390
408
|
const ssrError = createSSRErrorResponse(
|