@mandujs/core 0.18.3 → 0.18.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.
@@ -4,6 +4,7 @@
4
4
  */
5
5
 
6
6
  import type { ParsedResource } from "./parser";
7
+ import type { ResourceDefinition } from "./schema";
7
8
  import { generateResourceContract } from "./generators/contract";
8
9
  import { generateResourceTypes } from "./generators/types";
9
10
  import { generateResourceSlot } from "./generators/slot";
@@ -129,7 +130,7 @@ export async function generateResourceArtifacts(
129
130
  * Generate contract file
130
131
  */
131
132
  async function generateContract(
132
- definition: any,
133
+ definition: ResourceDefinition,
133
134
  resourceName: string,
134
135
  contractsDir: string,
135
136
  result: GeneratorResult
@@ -147,7 +148,7 @@ async function generateContract(
147
148
  * Generate types file
148
149
  */
149
150
  async function generateTypes(
150
- definition: any,
151
+ definition: ResourceDefinition,
151
152
  resourceName: string,
152
153
  typesDir: string,
153
154
  result: GeneratorResult
@@ -165,7 +166,7 @@ async function generateTypes(
165
166
  * Generate slot file (PRESERVE if exists!)
166
167
  */
167
168
  async function generateSlot(
168
- definition: any,
169
+ definition: ResourceDefinition,
169
170
  resourceName: string,
170
171
  slotsDir: string,
171
172
  force: boolean,
@@ -195,7 +196,7 @@ async function generateSlot(
195
196
  * Generate client file
196
197
  */
197
198
  async function generateClient(
198
- definition: any,
199
+ definition: ResourceDefinition,
199
200
  resourceName: string,
200
201
  clientDir: string,
201
202
  result: GeneratorResult
@@ -1,3 +1,15 @@
1
+ /**
2
+ * HTML 텍스트 콘텐츠 이스케이프
3
+ * <title>, <p> 등 텍스트 노드에 들어갈 문자열을 안전하게 처리.
4
+ * 속성값과 달리 " ' 는 이스케이프 불필요.
5
+ */
6
+ export function escapeHtmlText(value: string): string {
7
+ return value
8
+ .replace(/&/g, "&amp;")
9
+ .replace(/</g, "&lt;")
10
+ .replace(/>/g, "&gt;");
11
+ }
12
+
1
13
  /**
2
14
  * HTML 속성값 이스케이프
3
15
  * XSS 방지를 위해 HTML 속성값에 들어갈 문자열을 안전하게 처리
@@ -5,7 +5,7 @@ import type { ManduFilling } from "../filling/filling";
5
5
  import { ManduContext } from "../filling/context";
6
6
  import { Router } from "./router";
7
7
  import { renderSSR, renderStreamingResponse } from "./ssr";
8
- import { PageBoundary, DefaultLoading, DefaultError, type ErrorFallbackProps } from "./boundary";
8
+ import { type ErrorFallbackProps } from "./boundary";
9
9
  import React, { type ReactNode } from "react";
10
10
  import path from "path";
11
11
  import fs from "fs/promises";
@@ -5,7 +5,7 @@ import type { ReactElement } from "react";
5
5
  import type { BundleManifest } from "../bundler/types";
6
6
  import type { HydrationConfig, HydrationPriority } from "../spec/schema";
7
7
  import { PORTS, TIMEOUTS } from "../constants";
8
- import { escapeHtmlAttr, escapeJsonForInlineScript } from "./escape";
8
+ import { escapeHtmlAttr, escapeHtmlText, escapeJsonForInlineScript } from "./escape";
9
9
  import { REACT_INTERNALS_SHIM_SCRIPT } from "./shims";
10
10
 
11
11
  // Re-export streaming SSR utilities
@@ -243,7 +243,7 @@ export function renderToHTML(element: ReactElement, options: SSROptions = {}): s
243
243
  <head>
244
244
  <meta charset="UTF-8">
245
245
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
246
- <title>${escapeHtmlAttr(title)}</title>
246
+ <title>${escapeHtmlText(title)}</title>
247
247
  ${cssLinkTag}
248
248
  ${headTags}
249
249
  </head>
@@ -313,6 +313,15 @@ function generateHMRScript(port: number): string {
313
313
  var ws = null;
314
314
  var reconnectAttempts = 0;
315
315
  var maxReconnectAttempts = ${TIMEOUTS.HMR_MAX_RECONNECT};
316
+ var baseDelay = ${TIMEOUTS.HMR_RECONNECT_DELAY};
317
+
318
+ function scheduleReconnect() {
319
+ if (reconnectAttempts < maxReconnectAttempts) {
320
+ reconnectAttempts++;
321
+ var delay = Math.min(baseDelay * Math.pow(2, reconnectAttempts - 1), 30000);
322
+ setTimeout(connect, delay);
323
+ }
324
+ }
316
325
 
317
326
  function connect() {
318
327
  try {
@@ -327,19 +336,27 @@ function generateHMRScript(port: number): string {
327
336
  if (msg.type === 'reload' || msg.type === 'island-update') {
328
337
  console.log('[Mandu HMR] Reloading...');
329
338
  location.reload();
339
+ } else if (msg.type === 'css-update') {
340
+ var cssPath = (msg.data && msg.data.cssPath) || '/.mandu/client/globals.css';
341
+ var links = document.querySelectorAll('link[rel="stylesheet"]');
342
+ var updated = false;
343
+ for (var i = 0; i < links.length; i++) {
344
+ var href = links[i].getAttribute('href') || '';
345
+ var base = href.split('?')[0];
346
+ if (base === cssPath || href.includes('globals.css') || href.includes('.mandu/client')) {
347
+ links[i].setAttribute('href', base + '?t=' + Date.now());
348
+ updated = true;
349
+ }
350
+ }
351
+ if (!updated) location.reload();
330
352
  } else if (msg.type === 'error') {
331
- console.error('[Mandu HMR] Build error:', msg.data?.message);
353
+ console.error('[Mandu HMR] Build error:', msg.data && msg.data.message);
332
354
  }
333
355
  } catch(err) {}
334
356
  };
335
- ws.onclose = function() {
336
- if (reconnectAttempts < maxReconnectAttempts) {
337
- reconnectAttempts++;
338
- setTimeout(connect, ${TIMEOUTS.HMR_RECONNECT_DELAY} * reconnectAttempts);
339
- }
340
- };
357
+ ws.onclose = function() { scheduleReconnect(); };
341
358
  } catch(err) {
342
- setTimeout(connect, ${TIMEOUTS.HMR_RECONNECT_DELAY});
359
+ scheduleReconnect();
343
360
  }
344
361
  }
345
362
  connect();
@@ -158,6 +158,7 @@ export interface StreamingLoaderResult<T = unknown> {
158
158
  */
159
159
  function isJSONSerializable(value: unknown, path: string = "root", isDev: boolean = false): { valid: boolean; issues: string[] } {
160
160
  const issues: string[] = [];
161
+ const seen = new WeakSet<object>();
161
162
 
162
163
  function check(val: unknown, currentPath: string): void {
163
164
  if (val === undefined) {
@@ -198,6 +199,12 @@ function isJSONSerializable(value: unknown, path: string = "root", isDev: boolea
198
199
  }
199
200
 
200
201
  if (type === "object") {
202
+ // 순환 참조 감지 — 무한 재귀 방지
203
+ if (seen.has(val as object)) {
204
+ issues.push(`${currentPath}: 순환 참조가 감지되었습니다 (JSON 직렬화 불가)`);
205
+ return;
206
+ }
207
+ seen.add(val as object);
201
208
  for (const [key, v] of Object.entries(val as Record<string, unknown>)) {
202
209
  check(v, `${currentPath}.${key}`);
203
210
  }