@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
package/src/bundler/dev.ts
CHANGED
|
@@ -143,6 +143,9 @@ export async function startDevBundler(options: DevBundlerOptions): Promise<DevBu
|
|
|
143
143
|
// 파일 감시 설정
|
|
144
144
|
const watchers: fs.FSWatcher[] = [];
|
|
145
145
|
let debounceTimer: ReturnType<typeof setTimeout> | null = null;
|
|
146
|
+
// 동시 빌드 방지 (#121): 빌드 중에 변경 발생 시 다음 빌드 대기
|
|
147
|
+
let isBuilding = false;
|
|
148
|
+
let pendingBuildFile: string | null = null;
|
|
146
149
|
|
|
147
150
|
// 파일이 공통 디렉토리에 있는지 확인
|
|
148
151
|
const isInCommonDir = (filePath: string): boolean => {
|
|
@@ -157,9 +160,30 @@ export async function startDevBundler(options: DevBundlerOptions): Promise<DevBu
|
|
|
157
160
|
};
|
|
158
161
|
|
|
159
162
|
const handleFileChange = async (changedFile: string) => {
|
|
163
|
+
// 동시 빌드 방지 (#121): 빌드 중이면 대기 큐에 저장
|
|
164
|
+
if (isBuilding) {
|
|
165
|
+
pendingBuildFile = changedFile;
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
isBuilding = true;
|
|
170
|
+
try {
|
|
171
|
+
await _doBuild(changedFile);
|
|
172
|
+
} finally {
|
|
173
|
+
isBuilding = false;
|
|
174
|
+
// 빌드 중 대기 중인 파일이 있으면 즉시 처리
|
|
175
|
+
if (pendingBuildFile) {
|
|
176
|
+
const next = pendingBuildFile;
|
|
177
|
+
pendingBuildFile = null;
|
|
178
|
+
await handleFileChange(next);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
const _doBuild = async (changedFile: string) => {
|
|
160
184
|
const normalizedPath = changedFile.replace(/\\/g, "/");
|
|
161
185
|
|
|
162
|
-
// 공통 컴포넌트 디렉토리 변경 → 전체 재빌드
|
|
186
|
+
// 공통 컴포넌트 디렉토리 변경 → 전체 재빌드 (targetRouteIds 없이)
|
|
163
187
|
if (isInCommonDir(changedFile)) {
|
|
164
188
|
console.log(`\n🔄 Common file changed: ${path.basename(changedFile)}`);
|
|
165
189
|
console.log(` Rebuilding all islands...`);
|
|
@@ -223,13 +247,15 @@ export async function startDevBundler(options: DevBundlerOptions): Promise<DevBu
|
|
|
223
247
|
const route = manifest.routes.find((r) => r.id === routeId);
|
|
224
248
|
if (!route || !route.clientModule) return;
|
|
225
249
|
|
|
226
|
-
console.log(`\n🔄 Rebuilding: ${routeId}`);
|
|
250
|
+
console.log(`\n🔄 Rebuilding island: ${routeId}`);
|
|
227
251
|
const startTime = performance.now();
|
|
228
252
|
|
|
229
253
|
try {
|
|
254
|
+
// 단일 island만 재빌드 (Runtime/Router/Vendor 스킵, #122)
|
|
230
255
|
const result = await buildClientBundles(manifest, rootDir, {
|
|
231
256
|
minify: false,
|
|
232
257
|
sourcemap: true,
|
|
258
|
+
targetRouteIds: [routeId],
|
|
233
259
|
});
|
|
234
260
|
|
|
235
261
|
const buildTime = performance.now() - startTime;
|
|
@@ -286,7 +312,7 @@ export async function startDevBundler(options: DevBundlerOptions): Promise<DevBu
|
|
|
286
312
|
console.log(`👀 Watching ${watchers.length} directories for changes...`);
|
|
287
313
|
if (commonWatchDirs.size > 0) {
|
|
288
314
|
const commonDirNames = Array.from(commonWatchDirs)
|
|
289
|
-
.map(d => path.relative(rootDir, d) || ".")
|
|
315
|
+
.map(d => (path.relative(rootDir, d) || ".").replace(/\\/g, "/"))
|
|
290
316
|
.join(", ");
|
|
291
317
|
console.log(`📦 Common dirs (full rebuild): ${commonDirNames}`);
|
|
292
318
|
}
|
|
@@ -428,6 +454,7 @@ export function generateHMRClientScript(port: number): string {
|
|
|
428
454
|
let reconnectAttempts = 0;
|
|
429
455
|
const maxReconnectAttempts = ${TIMEOUTS.HMR_MAX_RECONNECT};
|
|
430
456
|
const reconnectDelay = ${TIMEOUTS.HMR_RECONNECT_DELAY};
|
|
457
|
+
const staleIslands = new Set();
|
|
431
458
|
|
|
432
459
|
function connect() {
|
|
433
460
|
try {
|
|
@@ -464,8 +491,9 @@ export function generateHMRClientScript(port: number): string {
|
|
|
464
491
|
function scheduleReconnect() {
|
|
465
492
|
if (reconnectAttempts < maxReconnectAttempts) {
|
|
466
493
|
reconnectAttempts++;
|
|
467
|
-
|
|
468
|
-
|
|
494
|
+
var delay = Math.min(reconnectDelay * Math.pow(2, reconnectAttempts - 1), 30000);
|
|
495
|
+
console.log('[Mandu HMR] Reconnecting in ' + delay + 'ms (' + reconnectAttempts + '/' + maxReconnectAttempts + ')');
|
|
496
|
+
setTimeout(connect, delay);
|
|
469
497
|
}
|
|
470
498
|
}
|
|
471
499
|
|
|
@@ -483,6 +511,7 @@ export function generateHMRClientScript(port: number): string {
|
|
|
483
511
|
case 'island-update':
|
|
484
512
|
const routeId = message.data?.routeId;
|
|
485
513
|
console.log('[Mandu HMR] Island updated:', routeId);
|
|
514
|
+
staleIslands.add(routeId);
|
|
486
515
|
|
|
487
516
|
// 현재 페이지의 island인지 확인
|
|
488
517
|
const island = document.querySelector('[data-mandu-island="' + routeId + '"]');
|
|
@@ -533,7 +562,19 @@ export function generateHMRClientScript(port: number): string {
|
|
|
533
562
|
const overlay = document.createElement('div');
|
|
534
563
|
overlay.id = 'mandu-hmr-error';
|
|
535
564
|
overlay.style.cssText = 'position:fixed;top:0;left:0;right:0;bottom:0;background:rgba(0,0,0,0.9);color:#ff6b6b;font-family:monospace;padding:40px;z-index:99999;overflow:auto;';
|
|
536
|
-
|
|
565
|
+
const h2 = document.createElement('h2');
|
|
566
|
+
h2.style.cssText = 'color:#ff6b6b;margin:0 0 20px;';
|
|
567
|
+
h2.textContent = '🔥 Build Error';
|
|
568
|
+
const pre = document.createElement('pre');
|
|
569
|
+
pre.style.cssText = 'white-space:pre-wrap;word-break:break-all;';
|
|
570
|
+
pre.textContent = message || 'Unknown error';
|
|
571
|
+
const btn = document.createElement('button');
|
|
572
|
+
btn.style.cssText = 'position:fixed;top:20px;right:20px;background:#333;color:#fff;border:none;padding:10px 20px;cursor:pointer;';
|
|
573
|
+
btn.textContent = 'Close';
|
|
574
|
+
btn.onclick = function() { overlay.remove(); };
|
|
575
|
+
overlay.appendChild(h2);
|
|
576
|
+
overlay.appendChild(pre);
|
|
577
|
+
overlay.appendChild(btn);
|
|
537
578
|
document.body.appendChild(overlay);
|
|
538
579
|
}
|
|
539
580
|
|
|
@@ -549,6 +590,22 @@ export function generateHMRClientScript(port: number): string {
|
|
|
549
590
|
if (ws) ws.close();
|
|
550
591
|
});
|
|
551
592
|
|
|
593
|
+
// 페이지 이동 시 stale island 감지 후 리로드 (#115)
|
|
594
|
+
function checkStaleIslandsOnNavigation() {
|
|
595
|
+
if (staleIslands.size === 0) return;
|
|
596
|
+
for (const id of staleIslands) {
|
|
597
|
+
if (document.querySelector('[data-mandu-island="' + id + '"]')) {
|
|
598
|
+
console.log('[Mandu HMR] Stale island detected after navigation, reloading...');
|
|
599
|
+
location.reload();
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
window.addEventListener('popstate', checkStaleIslandsOnNavigation);
|
|
605
|
+
window.addEventListener('pageshow', function(e) {
|
|
606
|
+
if (e.persisted) checkStaleIslandsOnNavigation();
|
|
607
|
+
});
|
|
608
|
+
|
|
552
609
|
// Ping 전송 (연결 유지)
|
|
553
610
|
setInterval(function() {
|
|
554
611
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
package/src/bundler/types.ts
CHANGED
|
@@ -111,4 +111,10 @@ export interface BundlerOptions {
|
|
|
111
111
|
* 주의: splitting=true인 경우 청크 파일 관리가 필요합니다.
|
|
112
112
|
*/
|
|
113
113
|
splitting?: boolean;
|
|
114
|
+
/**
|
|
115
|
+
* 빌드할 Island routeId 목록 (부분 빌드용, #122)
|
|
116
|
+
* - 지정 시 해당 Island만 재빌드 (Runtime/Router/Vendor 스킵)
|
|
117
|
+
* - 미지정 시 전체 빌드 (기본값)
|
|
118
|
+
*/
|
|
119
|
+
targetRouteIds?: string[];
|
|
114
120
|
}
|
package/src/config/mandu.ts
CHANGED
package/src/config/validate.ts
CHANGED
|
@@ -71,7 +71,7 @@ const ServerConfigSchema = z
|
|
|
71
71
|
*/
|
|
72
72
|
const GuardConfigSchema = z
|
|
73
73
|
.object({
|
|
74
|
-
preset: z.enum(["mandu", "fsd", "clean", "hexagonal", "atomic"]).default("mandu"),
|
|
74
|
+
preset: z.enum(["mandu", "fsd", "clean", "hexagonal", "atomic", "cqrs"]).default("mandu"),
|
|
75
75
|
srcDir: z.string().default("src"),
|
|
76
76
|
exclude: z.array(z.string()).default([]),
|
|
77
77
|
realtime: z.boolean().default(true),
|