@mandujs/core 0.18.5 → 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 +1 -1
- package/src/bundler/build.ts +46 -0
- package/src/bundler/css.ts +6 -1
- package/src/bundler/dev.ts +29 -3
- package/src/bundler/types.ts +6 -0
- package/src/router/fs-scanner.ts +4 -4
- package/src/runtime/streaming-ssr.ts +27 -7
package/package.json
CHANGED
package/src/bundler/build.ts
CHANGED
|
@@ -1205,6 +1205,52 @@ export async function buildClientBundles(
|
|
|
1205
1205
|
};
|
|
1206
1206
|
}
|
|
1207
1207
|
|
|
1208
|
+
// 부분 빌드 모드: targetRouteIds가 지정되면 해당 Island만 재빌드 (#122)
|
|
1209
|
+
if (options.targetRouteIds && options.targetRouteIds.length > 0) {
|
|
1210
|
+
const targetRoutes = hydratedRoutes.filter((r) => options.targetRouteIds!.includes(r.id));
|
|
1211
|
+
|
|
1212
|
+
for (const route of targetRoutes) {
|
|
1213
|
+
try {
|
|
1214
|
+
const result = await buildIsland(route, rootDir, outDir, options);
|
|
1215
|
+
outputs.push(result);
|
|
1216
|
+
} catch (error) {
|
|
1217
|
+
errors.push(`[${route.id}] ${String(error)}`);
|
|
1218
|
+
}
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
// 기존 매니페스트를 읽어 변경된 Island만 갱신
|
|
1222
|
+
let existingManifest: BundleManifest;
|
|
1223
|
+
try {
|
|
1224
|
+
const manifestData = await fs.readFile(path.join(rootDir, ".mandu/manifest.json"), "utf-8");
|
|
1225
|
+
existingManifest = JSON.parse(manifestData) as BundleManifest;
|
|
1226
|
+
} catch {
|
|
1227
|
+
// 기존 매니페스트 없으면 전체 빌드로 재시도 (targetRouteIds 제거)
|
|
1228
|
+
return buildClientBundles(manifest, rootDir, { ...options, targetRouteIds: undefined });
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
for (const output of outputs) {
|
|
1232
|
+
if (existingManifest.bundles[output.routeId]) {
|
|
1233
|
+
existingManifest.bundles[output.routeId].js = output.outputPath;
|
|
1234
|
+
} else {
|
|
1235
|
+
const route = targetRoutes.find((r) => r.id === output.routeId);
|
|
1236
|
+
const hydration = route ? getRouteHydration(route) : null;
|
|
1237
|
+
existingManifest.bundles[output.routeId] = {
|
|
1238
|
+
js: output.outputPath,
|
|
1239
|
+
dependencies: ["_runtime", "_react"],
|
|
1240
|
+
priority: hydration?.priority || HYDRATION.DEFAULT_PRIORITY,
|
|
1241
|
+
};
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
await fs.writeFile(
|
|
1246
|
+
path.join(rootDir, ".mandu/manifest.json"),
|
|
1247
|
+
JSON.stringify(existingManifest, null, 2)
|
|
1248
|
+
);
|
|
1249
|
+
|
|
1250
|
+
const stats = calculateStats(outputs, startTime);
|
|
1251
|
+
return { success: errors.length === 0, outputs, errors, manifest: existingManifest, stats };
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1208
1254
|
// 3-4. Runtime, Router, Vendor 번들 병렬 빌드 (서로 독립적)
|
|
1209
1255
|
const [runtimeResult, routerResult, vendorResult] = await Promise.all([
|
|
1210
1256
|
buildRuntime(outDir, options),
|
package/src/bundler/css.ts
CHANGED
|
@@ -311,7 +311,12 @@ export async function startCSSWatch(options: CSSBuildOptions): Promise<CSSWatche
|
|
|
311
311
|
serverPath: SERVER_CSS_PATH,
|
|
312
312
|
close: () => {
|
|
313
313
|
fsWatcher?.close();
|
|
314
|
-
|
|
314
|
+
// Windows에서는 SIGTERM이 무시될 수 있으므로 SIGKILL 사용 (#117)
|
|
315
|
+
if (process.platform === "win32") {
|
|
316
|
+
proc.kill("SIGKILL");
|
|
317
|
+
} else {
|
|
318
|
+
proc.kill();
|
|
319
|
+
}
|
|
315
320
|
},
|
|
316
321
|
};
|
|
317
322
|
}
|
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
|
}
|
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/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
|
/**
|
|
@@ -577,6 +577,7 @@ function generateDeferredDataScript(routeId: string, key: string, data: unknown)
|
|
|
577
577
|
|
|
578
578
|
/**
|
|
579
579
|
* HMR 스크립트 생성
|
|
580
|
+
* ssr.ts의 generateHMRScript와 동일한 구현을 유지해야 함 (#114)
|
|
580
581
|
*/
|
|
581
582
|
function generateHMRScript(port: number): string {
|
|
582
583
|
const hmrPort = port + PORTS.HMR_OFFSET;
|
|
@@ -585,6 +586,15 @@ function generateHMRScript(port: number): string {
|
|
|
585
586
|
var ws = null;
|
|
586
587
|
var reconnectAttempts = 0;
|
|
587
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
|
+
}
|
|
588
598
|
|
|
589
599
|
function connect() {
|
|
590
600
|
try {
|
|
@@ -599,17 +609,27 @@ function generateHMRScript(port: number): string {
|
|
|
599
609
|
if (msg.type === 'reload' || msg.type === 'island-update') {
|
|
600
610
|
console.log('[Mandu HMR] Reloading...');
|
|
601
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);
|
|
602
627
|
}
|
|
603
628
|
} catch(err) {}
|
|
604
629
|
};
|
|
605
|
-
ws.onclose = function() {
|
|
606
|
-
if (reconnectAttempts < maxReconnectAttempts) {
|
|
607
|
-
reconnectAttempts++;
|
|
608
|
-
setTimeout(connect, ${TIMEOUTS.HMR_RECONNECT_DELAY} * reconnectAttempts);
|
|
609
|
-
}
|
|
610
|
-
};
|
|
630
|
+
ws.onclose = function() { scheduleReconnect(); };
|
|
611
631
|
} catch(err) {
|
|
612
|
-
|
|
632
|
+
scheduleReconnect();
|
|
613
633
|
}
|
|
614
634
|
}
|
|
615
635
|
connect();
|