@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.
@@ -0,0 +1,404 @@
1
+ /**
2
+ * Mandu Props Serialization ๐Ÿ“ฆ
3
+ * Fresh ์Šคํƒ€์ผ ๊ณ ๊ธ‰ ์ง๋ ฌํ™”/์—ญ์ง๋ ฌํ™”
4
+ *
5
+ * @see https://fresh.deno.dev/docs/concepts/islands
6
+ *
7
+ * ์ง€์› ํƒ€์ž…:
8
+ * - ์›์‹œํ˜•: null, boolean, number, string, bigint, undefined
9
+ * - ํŠน์ˆ˜ ๊ฐ์ฒด: Date, URL, RegExp, Map, Set
10
+ * - ์ˆœํ™˜ ์ฐธ์กฐ
11
+ * - ์ค‘์ฒฉ ๊ฐ์ฒด/๋ฐฐ์—ด
12
+ */
13
+
14
+ // ============================================
15
+ // ํƒ€์ž… ๋งˆ์ปค
16
+ // ============================================
17
+
18
+ const TYPE_MARKERS = {
19
+ /** undefined */
20
+ UNDEFINED: "\x00_",
21
+ /** Date */
22
+ DATE: "\x00D",
23
+ /** URL */
24
+ URL: "\x00U",
25
+ /** RegExp */
26
+ REGEXP: "\x00R",
27
+ /** Map */
28
+ MAP: "\x00M",
29
+ /** Set */
30
+ SET: "\x00S",
31
+ /** ์ˆœํ™˜ ์ฐธ์กฐ */
32
+ REF: "\x00$",
33
+ /** BigInt */
34
+ BIGINT: "\x00B",
35
+ /** Symbol (์ œํ•œ์  ์ง€์›) */
36
+ SYMBOL: "\x00Y",
37
+ /** Error */
38
+ ERROR: "\x00E",
39
+ } as const;
40
+
41
+ // ============================================
42
+ // ์ง๋ ฌํ™”
43
+ // ============================================
44
+
45
+ /**
46
+ * ์ง๋ ฌํ™” ์ปจํ…์ŠคํŠธ (์ˆœํ™˜ ์ฐธ์กฐ ์ถ”์ )
47
+ */
48
+ interface SerializeContext {
49
+ /** ์ด๋ฏธ ๋ณธ ๊ฐ์ฒด โ†’ ์ธ๋ฑ์Šค */
50
+ seen: Map<object, number>;
51
+ /** ์ฐธ์กฐ ํ…Œ์ด๋ธ” */
52
+ refs: object[];
53
+ }
54
+
55
+ /**
56
+ * Props ์ง๋ ฌํ™”
57
+ *
58
+ * @example
59
+ * ```typescript
60
+ * const props = {
61
+ * date: new Date(),
62
+ * url: new URL('https://example.com'),
63
+ * items: new Set([1, 2, 3]),
64
+ * cache: new Map([['key', 'value']]),
65
+ * };
66
+ *
67
+ * const json = serializeProps(props);
68
+ * // ํด๋ผ์ด์–ธํŠธ๋กœ ์ „์†ก
69
+ * ```
70
+ */
71
+ export function serializeProps(props: Record<string, unknown>): string {
72
+ const ctx: SerializeContext = { seen: new Map(), refs: [] };
73
+ return JSON.stringify(serialize(props, ctx));
74
+ }
75
+
76
+ /**
77
+ * ๊ฐ’ ์ง๋ ฌํ™” (์žฌ๊ท€)
78
+ */
79
+ function serialize(value: unknown, ctx: SerializeContext): unknown {
80
+ // null
81
+ if (value === null) return null;
82
+
83
+ // undefined
84
+ if (value === undefined) return TYPE_MARKERS.UNDEFINED;
85
+
86
+ // ์›์‹œํ˜•
87
+ if (typeof value === "boolean" || typeof value === "number") {
88
+ return value;
89
+ }
90
+
91
+ if (typeof value === "string") {
92
+ // ํƒ€์ž… ๋งˆ์ปค์™€ ์ถฉ๋Œ ๋ฐฉ์ง€ (์ฒซ ๋ฌธ์ž๊ฐ€ \x00์ธ ๊ฒฝ์šฐ)
93
+ if (value.startsWith("\x00")) {
94
+ return "\x00\x00" + value;
95
+ }
96
+ return value;
97
+ }
98
+
99
+ if (typeof value === "bigint") {
100
+ return TYPE_MARKERS.BIGINT + value.toString();
101
+ }
102
+
103
+ if (typeof value === "symbol") {
104
+ // Symbol์€ description๋งŒ ๋ณด์กด
105
+ return TYPE_MARKERS.SYMBOL + (value.description ?? "");
106
+ }
107
+
108
+ // ํ•จ์ˆ˜๋Š” ์ง๋ ฌํ™” ๋ถˆ๊ฐ€
109
+ if (typeof value === "function") {
110
+ console.warn("[Mandu Serialize] Functions cannot be serialized, skipping");
111
+ return undefined;
112
+ }
113
+
114
+ // ๊ฐ์ฒด ์ˆœํ™˜ ์ฐธ์กฐ ์ฒดํฌ
115
+ if (typeof value === "object") {
116
+ const existing = ctx.seen.get(value);
117
+ if (existing !== undefined) {
118
+ return TYPE_MARKERS.REF + existing;
119
+ }
120
+
121
+ const idx = ctx.refs.length;
122
+ ctx.seen.set(value, idx);
123
+ ctx.refs.push(value);
124
+ }
125
+
126
+ // Date
127
+ if (value instanceof Date) {
128
+ return TYPE_MARKERS.DATE + value.toISOString();
129
+ }
130
+
131
+ // URL
132
+ if (value instanceof URL) {
133
+ return TYPE_MARKERS.URL + value.href;
134
+ }
135
+
136
+ // RegExp
137
+ if (value instanceof RegExp) {
138
+ return TYPE_MARKERS.REGEXP + value.toString();
139
+ }
140
+
141
+ // Error
142
+ if (value instanceof Error) {
143
+ return [
144
+ TYPE_MARKERS.ERROR,
145
+ value.name,
146
+ value.message,
147
+ value.stack ?? "",
148
+ ];
149
+ }
150
+
151
+ // Map
152
+ if (value instanceof Map) {
153
+ const entries: [unknown, unknown][] = [];
154
+ for (const [k, v] of value.entries()) {
155
+ entries.push([serialize(k, ctx), serialize(v, ctx)]);
156
+ }
157
+ return [TYPE_MARKERS.MAP, ...entries];
158
+ }
159
+
160
+ // Set
161
+ if (value instanceof Set) {
162
+ const items: unknown[] = [];
163
+ for (const item of value) {
164
+ items.push(serialize(item, ctx));
165
+ }
166
+ return [TYPE_MARKERS.SET, ...items];
167
+ }
168
+
169
+ // ๋ฐฐ์—ด
170
+ if (Array.isArray(value)) {
171
+ return value.map((item) => serialize(item, ctx));
172
+ }
173
+
174
+ // ์ผ๋ฐ˜ ๊ฐ์ฒด
175
+ const result: Record<string, unknown> = {};
176
+ for (const [k, v] of Object.entries(value as object)) {
177
+ const serialized = serialize(v, ctx);
178
+ if (serialized !== undefined) {
179
+ result[k] = serialized;
180
+ }
181
+ }
182
+ return result;
183
+ }
184
+
185
+ // ============================================
186
+ // ์—ญ์ง๋ ฌํ™”
187
+ // ============================================
188
+
189
+ /**
190
+ * ์—ญ์ง๋ ฌํ™” ์ปจํ…์ŠคํŠธ (์ˆœํ™˜ ์ฐธ์กฐ ๋ณต์›)
191
+ */
192
+ interface DeserializeContext {
193
+ refs: unknown[];
194
+ }
195
+
196
+ /**
197
+ * Props ์—ญ์ง๋ ฌํ™”
198
+ *
199
+ * @example
200
+ * ```typescript
201
+ * // ์„œ๋ฒ„์—์„œ ๋ฐ›์€ JSON
202
+ * const json = '{"date":"\x00D2025-01-28T00:00:00.000Z"}';
203
+ *
204
+ * const props = deserializeProps(json);
205
+ * console.log(props.date instanceof Date); // true
206
+ * ```
207
+ */
208
+ export function deserializeProps(json: string): Record<string, unknown> {
209
+ const ctx: DeserializeContext = { refs: [] };
210
+ const parsed = JSON.parse(json);
211
+ return deserialize(parsed, ctx) as Record<string, unknown>;
212
+ }
213
+
214
+ /**
215
+ * ๊ฐ’ ์—ญ์ง๋ ฌํ™” (์žฌ๊ท€)
216
+ */
217
+ function deserialize(value: unknown, ctx: DeserializeContext): unknown {
218
+ // null
219
+ if (value === null) return null;
220
+
221
+ // ๋ฌธ์ž์—ด โ†’ ํƒ€์ž… ๋งˆ์ปค ์ฒดํฌ
222
+ if (typeof value === "string") {
223
+ // undefined
224
+ if (value === TYPE_MARKERS.UNDEFINED) return undefined;
225
+
226
+ // ์ด์Šค์ผ€์ดํ”„๋œ ๋ฌธ์ž์—ด (\x00\x00 โ†’ \x00)
227
+ if (value.startsWith("\x00\x00")) {
228
+ return value.slice(2);
229
+ }
230
+
231
+ // Date
232
+ if (value.startsWith(TYPE_MARKERS.DATE)) {
233
+ return new Date(value.slice(2));
234
+ }
235
+
236
+ // URL
237
+ if (value.startsWith(TYPE_MARKERS.URL)) {
238
+ return new URL(value.slice(2));
239
+ }
240
+
241
+ // RegExp
242
+ if (value.startsWith(TYPE_MARKERS.REGEXP)) {
243
+ const str = value.slice(2);
244
+ const match = str.match(/^\/(.*)\/([gimsuy]*)$/);
245
+ if (match) {
246
+ return new RegExp(match[1], match[2]);
247
+ }
248
+ return str; // ํŒŒ์‹ฑ ์‹คํŒจ ์‹œ ๋ฌธ์ž์—ด ๋ฐ˜ํ™˜
249
+ }
250
+
251
+ // BigInt
252
+ if (value.startsWith(TYPE_MARKERS.BIGINT)) {
253
+ return BigInt(value.slice(2));
254
+ }
255
+
256
+ // Symbol
257
+ if (value.startsWith(TYPE_MARKERS.SYMBOL)) {
258
+ return Symbol(value.slice(2));
259
+ }
260
+
261
+ // ์ˆœํ™˜ ์ฐธ์กฐ
262
+ if (value.startsWith(TYPE_MARKERS.REF)) {
263
+ const idx = parseInt(value.slice(2), 10);
264
+ return ctx.refs[idx];
265
+ }
266
+
267
+ return value;
268
+ }
269
+
270
+ // ์›์‹œํ˜•
271
+ if (typeof value === "boolean" || typeof value === "number") {
272
+ return value;
273
+ }
274
+
275
+ // ๋ฐฐ์—ด โ†’ ํŠน์ˆ˜ ํƒ€์ž… ์ฒดํฌ
276
+ if (Array.isArray(value)) {
277
+ const marker = value[0];
278
+
279
+ // Error
280
+ if (marker === TYPE_MARKERS.ERROR) {
281
+ const [, name, message, stack] = value as [string, string, string, string];
282
+ const error = new Error(message);
283
+ error.name = name;
284
+ if (stack) error.stack = stack;
285
+ ctx.refs.push(error);
286
+ return error;
287
+ }
288
+
289
+ // Map
290
+ if (marker === TYPE_MARKERS.MAP) {
291
+ const map = new Map();
292
+ ctx.refs.push(map);
293
+ for (let i = 1; i < value.length; i++) {
294
+ const [k, v] = value[i] as [unknown, unknown];
295
+ map.set(deserialize(k, ctx), deserialize(v, ctx));
296
+ }
297
+ return map;
298
+ }
299
+
300
+ // Set
301
+ if (marker === TYPE_MARKERS.SET) {
302
+ const set = new Set();
303
+ ctx.refs.push(set);
304
+ for (let i = 1; i < value.length; i++) {
305
+ set.add(deserialize(value[i], ctx));
306
+ }
307
+ return set;
308
+ }
309
+
310
+ // ์ผ๋ฐ˜ ๋ฐฐ์—ด
311
+ const arr: unknown[] = [];
312
+ ctx.refs.push(arr);
313
+ for (const item of value) {
314
+ arr.push(deserialize(item, ctx));
315
+ }
316
+ return arr;
317
+ }
318
+
319
+ // ์ผ๋ฐ˜ ๊ฐ์ฒด
320
+ if (typeof value === "object") {
321
+ const obj: Record<string, unknown> = {};
322
+ ctx.refs.push(obj);
323
+ for (const [k, v] of Object.entries(value)) {
324
+ obj[k] = deserialize(v, ctx);
325
+ }
326
+ return obj;
327
+ }
328
+
329
+ return value;
330
+ }
331
+
332
+ // ============================================
333
+ // ์œ ํ‹ธ๋ฆฌํ‹ฐ
334
+ // ============================================
335
+
336
+ /**
337
+ * ์ง๋ ฌํ™” ๊ฐ€๋Šฅ ์—ฌ๋ถ€ ์ฒดํฌ
338
+ */
339
+ export function isSerializable(value: unknown): boolean {
340
+ if (value === null || value === undefined) return true;
341
+
342
+ const type = typeof value;
343
+ if (type === "boolean" || type === "number" || type === "string" || type === "bigint") {
344
+ return true;
345
+ }
346
+
347
+ if (type === "function" || type === "symbol") {
348
+ return false;
349
+ }
350
+
351
+ if (value instanceof Date || value instanceof URL || value instanceof RegExp) {
352
+ return true;
353
+ }
354
+
355
+ if (value instanceof Map || value instanceof Set) {
356
+ return true;
357
+ }
358
+
359
+ if (Array.isArray(value)) {
360
+ return value.every(isSerializable);
361
+ }
362
+
363
+ if (type === "object") {
364
+ return Object.values(value as object).every(isSerializable);
365
+ }
366
+
367
+ return false;
368
+ }
369
+
370
+ /**
371
+ * SSR์—์„œ ํด๋ผ์ด์–ธํŠธ๋กœ props ์ „๋‹ฌ์šฉ ์Šคํฌ๋ฆฝํŠธ ์ƒ์„ฑ
372
+ */
373
+ export function generatePropsScript(
374
+ islandId: string,
375
+ props: Record<string, unknown>
376
+ ): string {
377
+ const json = serializeProps(props);
378
+ const escaped = json
379
+ .replace(/</g, "\\u003c")
380
+ .replace(/>/g, "\\u003e")
381
+ .replace(/&/g, "\\u0026");
382
+
383
+ return `<script type="application/json" data-mandu-props="${islandId}">${escaped}</script>`;
384
+ }
385
+
386
+ /**
387
+ * ํด๋ผ์ด์–ธํŠธ์—์„œ props ์Šคํฌ๋ฆฝํŠธ ํŒŒ์‹ฑ
388
+ */
389
+ export function parsePropsScript(islandId: string): Record<string, unknown> | null {
390
+ if (typeof document === "undefined") return null;
391
+
392
+ const script = document.querySelector(
393
+ `script[data-mandu-props="${islandId}"]`
394
+ ) as HTMLScriptElement | null;
395
+
396
+ if (!script?.textContent) return null;
397
+
398
+ try {
399
+ return deserializeProps(script.textContent);
400
+ } catch (err) {
401
+ console.error(`[Mandu] Failed to parse props for island ${islandId}:`, err);
402
+ return null;
403
+ }
404
+ }
@@ -7,6 +7,16 @@ import { ManduContext, NEXT_SYMBOL, 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 LifecycleStore,
12
+ type OnRequestHandler,
13
+ type BeforeHandleHandler,
14
+ type AfterHandleHandler,
15
+ type OnErrorHandler,
16
+ type AfterResponseHandler,
17
+ createLifecycleStore,
18
+ executeLifecycle,
19
+ } from "../runtime/lifecycle";
10
20
 
11
21
  /** Handler function type */
12
22
  export type Handler = (ctx: ManduContext) => Response | Promise<Response>;
@@ -41,6 +51,7 @@ interface FillingConfig<TLoaderData = unknown> {
41
51
  guards: Guard[];
42
52
  methodGuards: Map<HttpMethod, Guard[]>;
43
53
  loader?: Loader<TLoaderData>;
54
+ lifecycle: LifecycleStore;
44
55
  }
45
56
 
46
57
  /**
@@ -68,6 +79,7 @@ export class ManduFilling<TLoaderData = unknown> {
68
79
  handlers: new Map(),
69
80
  guards: [],
70
81
  methodGuards: new Map(),
82
+ lifecycle: createLifecycleStore(),
71
83
  };
72
84
 
73
85
  // ============================================
@@ -218,6 +230,90 @@ export class ManduFilling<TLoaderData = unknown> {
218
230
  return this.guard(guardFn, ...methods);
219
231
  }
220
232
 
233
+ // ============================================
234
+ // ๐ŸฅŸ Lifecycle Hooks (Elysia ์Šคํƒ€์ผ)
235
+ // ============================================
236
+
237
+ /**
238
+ * ์š”์ฒญ ์‹œ์ž‘ ์‹œ ์‹คํ–‰
239
+ * @example
240
+ * ```typescript
241
+ * .onRequest((ctx) => {
242
+ * console.log('Request:', ctx.req.method, ctx.req.url);
243
+ * })
244
+ * ```
245
+ */
246
+ onRequest(fn: OnRequestHandler): this {
247
+ this.config.lifecycle.onRequest.push({ fn, scope: "local" });
248
+ return this;
249
+ }
250
+
251
+ /**
252
+ * ํ•ธ๋“ค๋Ÿฌ ์ „ ์‹คํ–‰ (Guard ์—ญํ• )
253
+ * Response ๋ฐ˜ํ™˜ ์‹œ ์ฒด์ธ ์ค‘๋‹จ
254
+ * @example
255
+ * ```typescript
256
+ * .beforeHandle((ctx) => {
257
+ * if (!ctx.get('user')) {
258
+ * return ctx.unauthorized();
259
+ * }
260
+ * // void ๋ฐ˜ํ™˜ ์‹œ ๊ณ„์† ์ง„ํ–‰
261
+ * })
262
+ * ```
263
+ */
264
+ beforeHandle(fn: BeforeHandleHandler): this {
265
+ this.config.lifecycle.beforeHandle.push({ fn, scope: "local" });
266
+ return this;
267
+ }
268
+
269
+ /**
270
+ * ํ•ธ๋“ค๋Ÿฌ ํ›„ ์‹คํ–‰ (์‘๋‹ต ๋ณ€ํ™˜)
271
+ * @example
272
+ * ```typescript
273
+ * .afterHandle((ctx, response) => {
274
+ * // ์‘๋‹ต ํ—ค๋” ์ถ”๊ฐ€
275
+ * response.headers.set('X-Request-Id', crypto.randomUUID());
276
+ * return response;
277
+ * })
278
+ * ```
279
+ */
280
+ afterHandle(fn: AfterHandleHandler): this {
281
+ this.config.lifecycle.afterHandle.push({ fn, scope: "local" });
282
+ return this;
283
+ }
284
+
285
+ /**
286
+ * ์—๋Ÿฌ ๋ฐœ์ƒ ์‹œ ์‹คํ–‰
287
+ * Response ๋ฐ˜ํ™˜ ์‹œ ์—๋Ÿฌ ์‘๋‹ต์œผ๋กœ ์‚ฌ์šฉ
288
+ * @example
289
+ * ```typescript
290
+ * .onError((ctx, error) => {
291
+ * console.error('Error:', error);
292
+ * return ctx.json({ error: error.message }, 500);
293
+ * })
294
+ * ```
295
+ */
296
+ onError(fn: OnErrorHandler): this {
297
+ this.config.lifecycle.onError.push({ fn, scope: "local" });
298
+ return this;
299
+ }
300
+
301
+ /**
302
+ * ์‘๋‹ต ํ›„ ์‹คํ–‰ (๋น„๋™๊ธฐ, ์‘๋‹ต์— ์˜ํ–ฅ ์—†์Œ)
303
+ * ๋กœ๊น…, ๋ฉ”ํŠธ๋ฆญ ์ˆ˜์ง‘ ๋“ฑ์— ์‚ฌ์šฉ
304
+ * @example
305
+ * ```typescript
306
+ * .afterResponse((ctx) => {
307
+ * console.log('Response sent:', ctx.req.url);
308
+ * metrics.increment('requests');
309
+ * })
310
+ * ```
311
+ */
312
+ afterResponse(fn: AfterResponseHandler): this {
313
+ this.config.lifecycle.afterResponse.push({ fn, scope: "local" });
314
+ return this;
315
+ }
316
+
221
317
  // ============================================
222
318
  // ๐ŸฅŸ Execution
223
319
  // ============================================