@mandujs/core 0.18.3 → 0.18.6

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
@@ -335,7 +335,7 @@ export class FSScanner {
335
335
  // 루트 레이아웃
336
336
  const rootLayout = layoutMap.get(".");
337
337
  if (rootLayout) {
338
- chain.push(join(this.config.routesDir, rootLayout.relativePath));
338
+ chain.push(join(this.config.routesDir, rootLayout.relativePath).replace(/\\/g, "/"));
339
339
  }
340
340
 
341
341
  // 중첩 레이아웃
@@ -344,7 +344,7 @@ export class FSScanner {
344
344
  currentPath = currentPath ? `${currentPath}/${segment.raw}` : segment.raw;
345
345
  const layout = layoutMap.get(currentPath);
346
346
  if (layout) {
347
- chain.push(join(this.config.routesDir, layout.relativePath));
347
+ chain.push(join(this.config.routesDir, layout.relativePath).replace(/\\/g, "/"));
348
348
  }
349
349
  }
350
350
 
@@ -364,7 +364,7 @@ export class FSScanner {
364
364
  while (currentPath) {
365
365
  const file = fileMap.get(currentPath);
366
366
  if (file) {
367
- return join(this.config.routesDir, file.relativePath);
367
+ return join(this.config.routesDir, file.relativePath).replace(/\\/g, "/");
368
368
  }
369
369
  // 상위 디렉토리로
370
370
  const lastSlash = currentPath.lastIndexOf("/");
@@ -373,7 +373,7 @@ export class FSScanner {
373
373
 
374
374
  // 루트 체크
375
375
  const rootFile = fileMap.get(".");
376
- return rootFile ? join(this.config.routesDir, rootFile.relativePath) : undefined;
376
+ return rootFile ? join(this.config.routesDir, rootFile.relativePath).replace(/\\/g, "/") : undefined;
377
377
  }
378
378
 
379
379
  /**
@@ -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
  }
@@ -570,6 +577,7 @@ function generateDeferredDataScript(routeId: string, key: string, data: unknown)
570
577
 
571
578
  /**
572
579
  * HMR 스크립트 생성
580
+ * ssr.ts의 generateHMRScript와 동일한 구현을 유지해야 함 (#114)
573
581
  */
574
582
  function generateHMRScript(port: number): string {
575
583
  const hmrPort = port + PORTS.HMR_OFFSET;
@@ -578,6 +586,15 @@ function generateHMRScript(port: number): string {
578
586
  var ws = null;
579
587
  var reconnectAttempts = 0;
580
588
  var maxReconnectAttempts = ${TIMEOUTS.HMR_MAX_RECONNECT};
589
+ var baseDelay = ${TIMEOUTS.HMR_RECONNECT_DELAY};
590
+
591
+ function scheduleReconnect() {
592
+ if (reconnectAttempts < maxReconnectAttempts) {
593
+ reconnectAttempts++;
594
+ var delay = Math.min(baseDelay * Math.pow(2, reconnectAttempts - 1), 30000);
595
+ setTimeout(connect, delay);
596
+ }
597
+ }
581
598
 
582
599
  function connect() {
583
600
  try {
@@ -592,17 +609,27 @@ function generateHMRScript(port: number): string {
592
609
  if (msg.type === 'reload' || msg.type === 'island-update') {
593
610
  console.log('[Mandu HMR] Reloading...');
594
611
  location.reload();
612
+ } else if (msg.type === 'css-update') {
613
+ var cssPath = (msg.data && msg.data.cssPath) || '/.mandu/client/globals.css';
614
+ var links = document.querySelectorAll('link[rel="stylesheet"]');
615
+ var updated = false;
616
+ for (var i = 0; i < links.length; i++) {
617
+ var href = links[i].getAttribute('href') || '';
618
+ var base = href.split('?')[0];
619
+ if (base === cssPath || href.includes('globals.css') || href.includes('.mandu/client')) {
620
+ links[i].setAttribute('href', base + '?t=' + Date.now());
621
+ updated = true;
622
+ }
623
+ }
624
+ if (!updated) location.reload();
625
+ } else if (msg.type === 'error') {
626
+ console.error('[Mandu HMR] Build error:', msg.data && msg.data.message);
595
627
  }
596
628
  } catch(err) {}
597
629
  };
598
- ws.onclose = function() {
599
- if (reconnectAttempts < maxReconnectAttempts) {
600
- reconnectAttempts++;
601
- setTimeout(connect, ${TIMEOUTS.HMR_RECONNECT_DELAY} * reconnectAttempts);
602
- }
603
- };
630
+ ws.onclose = function() { scheduleReconnect(); };
604
631
  } catch(err) {
605
- setTimeout(connect, ${TIMEOUTS.HMR_RECONNECT_DELAY});
632
+ scheduleReconnect();
606
633
  }
607
634
  }
608
635
  connect();