@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.
- package/package.json +8 -2
- package/src/bundler/build.ts +53 -16
- package/src/bundler/css.ts +337 -302
- package/src/bundler/dev.ts +63 -6
- package/src/bundler/types.ts +6 -0
- package/src/config/mandu.ts +1 -1
- package/src/config/validate.ts +1 -1
- package/src/contract/registry.ts +591 -568
- package/src/resource/generator.ts +5 -4
- package/src/router/fs-scanner.ts +4 -4
- package/src/runtime/escape.ts +12 -0
- package/src/runtime/server.ts +1 -1
- package/src/runtime/ssr.ts +27 -10
- package/src/runtime/streaming-ssr.ts +34 -7
|
@@ -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:
|
|
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:
|
|
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:
|
|
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:
|
|
199
|
+
definition: ResourceDefinition,
|
|
199
200
|
resourceName: string,
|
|
200
201
|
clientDir: string,
|
|
201
202
|
result: GeneratorResult
|
package/src/router/fs-scanner.ts
CHANGED
|
@@ -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
|
/**
|
package/src/runtime/escape.ts
CHANGED
|
@@ -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, "&")
|
|
9
|
+
.replace(/</g, "<")
|
|
10
|
+
.replace(/>/g, ">");
|
|
11
|
+
}
|
|
12
|
+
|
|
1
13
|
/**
|
|
2
14
|
* HTML 속성값 이스케이프
|
|
3
15
|
* XSS 방지를 위해 HTML 속성값에 들어갈 문자열을 안전하게 처리
|
package/src/runtime/server.ts
CHANGED
|
@@ -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 {
|
|
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";
|
package/src/runtime/ssr.ts
CHANGED
|
@@ -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>${
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
632
|
+
scheduleReconnect();
|
|
606
633
|
}
|
|
607
634
|
}
|
|
608
635
|
connect();
|