@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mandujs/core",
3
- "version": "0.18.5",
3
+ "version": "0.18.6",
4
4
  "description": "Mandu Framework Core - Spec, Generator, Guard, Runtime",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -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),
@@ -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
- proc.kill();
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
  }
@@ -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
  }
@@ -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
  }
@@ -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
- setTimeout(connect, ${TIMEOUTS.HMR_RECONNECT_DELAY});
632
+ scheduleReconnect();
613
633
  }
614
634
  }
615
635
  connect();