@simplysm/sd-cli 13.0.64 → 13.0.66

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.
Files changed (39) hide show
  1. package/dist/orchestrators/DevOrchestrator.d.ts +0 -1
  2. package/dist/orchestrators/DevOrchestrator.d.ts.map +1 -1
  3. package/dist/orchestrators/DevOrchestrator.js +4 -12
  4. package/dist/orchestrators/DevOrchestrator.js.map +1 -1
  5. package/dist/utils/esbuild-config.d.ts +1 -1
  6. package/dist/utils/esbuild-config.d.ts.map +1 -1
  7. package/dist/utils/esbuild-config.js +3 -0
  8. package/dist/utils/esbuild-config.js.map +1 -1
  9. package/dist/utils/package-utils.d.ts +5 -9
  10. package/dist/utils/package-utils.d.ts.map +1 -1
  11. package/dist/utils/package-utils.js +43 -13
  12. package/dist/utils/package-utils.js.map +1 -1
  13. package/dist/utils/tailwind-config-deps.d.ts +1 -1
  14. package/dist/utils/tailwind-config-deps.d.ts.map +1 -1
  15. package/dist/utils/tailwind-config-deps.js +5 -3
  16. package/dist/utils/tailwind-config-deps.js.map +1 -1
  17. package/dist/utils/vite-config.d.ts +3 -3
  18. package/dist/utils/vite-config.d.ts.map +1 -1
  19. package/dist/utils/vite-config.js +43 -52
  20. package/dist/utils/vite-config.js.map +1 -1
  21. package/dist/workers/client.worker.d.ts +2 -2
  22. package/dist/workers/client.worker.d.ts.map +1 -1
  23. package/dist/workers/client.worker.js +3 -1
  24. package/dist/workers/client.worker.js.map +1 -1
  25. package/dist/workers/server.worker.d.ts +2 -2
  26. package/dist/workers/server.worker.d.ts.map +1 -1
  27. package/dist/workers/server.worker.js +57 -30
  28. package/dist/workers/server.worker.js.map +1 -1
  29. package/package.json +4 -4
  30. package/src/orchestrators/DevOrchestrator.ts +4 -16
  31. package/src/utils/esbuild-config.ts +4 -2
  32. package/src/utils/package-utils.ts +62 -22
  33. package/src/utils/tailwind-config-deps.ts +6 -3
  34. package/src/utils/vite-config.ts +64 -92
  35. package/src/workers/client.worker.ts +7 -3
  36. package/src/workers/server.worker.ts +78 -38
  37. package/templates/add-client/__CLIENT__/package.json.hbs +1 -1
  38. package/templates/add-server/__SERVER__/package.json.hbs +2 -2
  39. package/templates/init/package.json.hbs +3 -3
@@ -17,14 +17,14 @@ import { FsWatcher, pathNorm } from "@simplysm/core-node";
17
17
  * preset 등으로 참조하는 scope 패키지의 config 변경을 감지하지 못한다.
18
18
  * 이 플러그인이 해당 파일들을 watch하고, 변경 시 Tailwind 캐시를 무효화한다.
19
19
  */
20
- function sdTailwindConfigDepsPlugin(pkgDir: string, scopes: string[]): Plugin {
20
+ function sdTailwindConfigDepsPlugin(pkgDir: string, replaceDeps: string[]): Plugin {
21
21
  return {
22
22
  name: "sd-tailwind-config-deps",
23
23
  configureServer(server) {
24
24
  const configPath = path.join(pkgDir, "tailwind.config.ts");
25
25
  if (!fs.existsSync(configPath)) return;
26
26
 
27
- const allDeps = getTailwindConfigDeps(configPath, scopes);
27
+ const allDeps = getTailwindConfigDeps(configPath, replaceDeps);
28
28
  const configAbsolute = path.resolve(configPath);
29
29
  const externalDeps = allDeps.filter((d) => d !== configAbsolute);
30
30
  if (externalDeps.length === 0) return;
@@ -133,66 +133,51 @@ function sdPublicDevPlugin(pkgDir: string): Plugin {
133
133
  * 변경 시 Vite의 내부 HMR 파이프라인을 트리거한다.
134
134
  * optimizeDeps에서 제외하여 pre-bundled 캐시로 인한 변경 무시를 방지한다.
135
135
  */
136
- function sdScopeWatchPlugin(pkgDir: string, scopes: string[], onScopeRebuild?: () => void): Plugin {
136
+ function sdScopeWatchPlugin(
137
+ pkgDir: string,
138
+ replaceDeps: string[],
139
+ onScopeRebuild?: () => void,
140
+ ): Plugin {
137
141
  return {
138
142
  name: "sd-scope-watch",
139
143
  config() {
140
144
  const excluded: string[] = [];
141
145
  const nestedDepsToInclude: string[] = [];
142
146
 
143
- for (const scope of scopes) {
144
- // scope 패키지를 pre-bundling에서 제외하여 소스 코드로 취급
145
- const scopeDir = path.join(pkgDir, "node_modules", scope);
146
- if (!fs.existsSync(scopeDir)) continue;
147
-
148
- for (const name of fs.readdirSync(scopeDir)) {
149
- excluded.push(`${scope}/${name}`);
150
-
151
- // excluded 패키지의 dependencies를 nested include로 추가하여 pre-bundling 보장
152
- // Vite nested dependency 구문: "excluded-pkg > dep"
153
- // (pnpm strict 모듈 격리에서 transitive dep resolve하기 위해 필요)
154
- const depPkgJsonPath = path.join(scopeDir, name, "package.json");
155
- try {
156
- const depPkgJson = JSON.parse(fs.readFileSync(depPkgJsonPath, "utf-8")) as {
157
- dependencies?: Record<string, string>;
158
- };
159
- const excludedPkg = `${scope}/${name}`;
160
- for (const dep of Object.keys(depPkgJson.dependencies ?? {})) {
161
- // 같은 scope 내 패키지는 이미 excluded이므로 제외
162
- if (scopes.some((s) => dep.startsWith(`${s}/`))) continue;
163
- // SolidJS 관련 패키지는 solid 플러그인 transform이 필요하므로 pre-bundling 불가
164
- if (dep === "solid-js" || dep.startsWith("@solidjs/") || dep.startsWith("solid-"))
165
- continue;
166
- // PostCSS/빌드 도구는 브라우저 pre-bundling 대상 아님
167
- if (dep === "tailwindcss") continue;
168
-
169
- // subpath-only 패키지 필터링: 두 경로를 시도하여 확인
170
- // pnpm 구조에서 realpath를 따라 .pnpm node_modules에서 먼저 찾기
171
- const realPkgPath = fs.realpathSync(path.join(scopeDir, name));
172
- const pnpmNodeModules = path.resolve(realPkgPath, "../..");
173
- const depPkgJsonResolved = path.join(pnpmNodeModules, dep, "package.json");
174
- if (isSubpathOnlyPackage(depPkgJsonResolved)) {
175
- continue;
176
- }
177
-
178
- // workspace 패키지는 realpath가 소스 디렉토리로 해석되어 .pnpm 구조가 아님
179
- // symlink 경로의 node_modules에서 fallback 시도
180
- const depPkgJsonFallback = path.join(
181
- scopeDir,
182
- name,
183
- "node_modules",
184
- dep,
185
- "package.json",
186
- );
187
- if (isSubpathOnlyPackage(depPkgJsonFallback)) {
188
- continue;
189
- }
190
-
191
- nestedDepsToInclude.push(`${excludedPkg} > ${dep}`);
192
- }
193
- } catch {
194
- // package.json 읽기 실패 시 스킵
147
+ for (const pkg of replaceDeps) {
148
+ excluded.push(pkg);
149
+
150
+ const pkgParts = pkg.split("/");
151
+ const depPkgJsonPath = path.join(pkgDir, "node_modules", ...pkgParts, "package.json");
152
+ try {
153
+ const depPkgJson = JSON.parse(fs.readFileSync(depPkgJsonPath, "utf-8")) as {
154
+ dependencies?: Record<string, string>;
155
+ };
156
+ for (const dep of Object.keys(depPkgJson.dependencies ?? {})) {
157
+ if (replaceDeps.includes(dep)) continue;
158
+ if (dep === "solid-js" || dep.startsWith("@solidjs/") || dep.startsWith("solid-"))
159
+ continue;
160
+ if (dep === "tailwindcss") continue;
161
+
162
+ const realPkgPath = fs.realpathSync(path.join(pkgDir, "node_modules", ...pkgParts));
163
+ const pnpmNodeModules = path.resolve(realPkgPath, "../..");
164
+ const depPkgJsonResolved = path.join(pnpmNodeModules, dep, "package.json");
165
+ if (isSubpathOnlyPackage(depPkgJsonResolved)) continue;
166
+
167
+ const depPkgJsonFallback = path.join(
168
+ pkgDir,
169
+ "node_modules",
170
+ ...pkgParts,
171
+ "node_modules",
172
+ dep,
173
+ "package.json",
174
+ );
175
+ if (isSubpathOnlyPackage(depPkgJsonFallback)) continue;
176
+
177
+ nestedDepsToInclude.push(`${pkg} > ${dep}`);
195
178
  }
179
+ } catch {
180
+ // package.json 읽기 실패 시 스킵
196
181
  }
197
182
  }
198
183
 
@@ -207,56 +192,43 @@ function sdScopeWatchPlugin(pkgDir: string, scopes: string[], onScopeRebuild?: (
207
192
  async configureServer(server) {
208
193
  const watchPaths: string[] = [];
209
194
 
210
- for (const scope of scopes) {
211
- const scopeDir = path.join(pkgDir, "node_modules", scope);
212
- if (!fs.existsSync(scopeDir)) continue;
213
-
214
- for (const pkgName of fs.readdirSync(scopeDir)) {
215
- const pkgRoot = path.join(scopeDir, pkgName);
195
+ for (const pkg of replaceDeps) {
196
+ const pkgParts = pkg.split("/");
197
+ const pkgRoot = path.join(pkgDir, "node_modules", ...pkgParts);
198
+ if (!fs.existsSync(pkgRoot)) continue;
216
199
 
217
- // dist 디렉토리 watch (JS/TS 빌드 결과물)
218
- const distDir = path.join(pkgRoot, "dist");
219
- if (fs.existsSync(distDir)) {
220
- watchPaths.push(distDir);
221
- }
200
+ const distDir = path.join(pkgRoot, "dist");
201
+ if (fs.existsSync(distDir)) {
202
+ watchPaths.push(distDir);
203
+ }
222
204
 
223
- // 패키지 루트의 CSS/config 파일 watch (tailwind.css, tailwind.config.ts 등)
224
- for (const file of fs.readdirSync(pkgRoot)) {
225
- if (
226
- file.endsWith(".css") ||
227
- file === "tailwind.config.ts" ||
228
- file === "tailwind.config.js"
229
- ) {
230
- watchPaths.push(path.join(pkgRoot, file));
231
- }
205
+ for (const file of fs.readdirSync(pkgRoot)) {
206
+ if (
207
+ file.endsWith(".css") ||
208
+ file === "tailwind.config.ts" ||
209
+ file === "tailwind.config.js"
210
+ ) {
211
+ watchPaths.push(path.join(pkgRoot, file));
232
212
  }
233
213
  }
234
214
  }
235
215
 
236
216
  if (watchPaths.length === 0) return;
237
217
 
238
- // Vite의 기본 watcher는 **/node_modules/**를 ignore하고
239
- // server.watcher.add()로는 이 패턴을 override할 수 없다.
240
- // 별도의 FsWatcher로 scope 패키지의 dist 디렉토리를 감시한다.
241
218
  const scopeWatcher = await FsWatcher.watch(watchPaths);
242
219
  scopeWatcher.onChange({ delay: 300 }, (changeInfos) => {
243
220
  for (const { path: changedPath } of changeInfos) {
244
- // pnpm symlink → real path 변환 (Vite module graph은 real path 사용)
245
221
  let realPath: string;
246
222
  try {
247
223
  realPath = fs.realpathSync(changedPath);
248
224
  } catch {
249
- continue; // 삭제된 파일
225
+ continue;
250
226
  }
251
-
252
- // Vite의 내부 HMR 파이프라인 트리거
253
227
  server.watcher.emit("change", realPath);
254
228
  }
255
-
256
229
  onScopeRebuild?.();
257
230
  });
258
231
 
259
- // 서버 종료 시 watcher 정리
260
232
  server.httpServer?.on("close", () => void scopeWatcher.close());
261
233
  },
262
234
  };
@@ -274,9 +246,9 @@ export interface ViteConfigOptions {
274
246
  mode: "build" | "dev";
275
247
  /** dev 모드일 때 서버 포트 (0이면 자동 할당) */
276
248
  serverPort?: number;
277
- /** watch 대상 scope 목록 (예: ["@myapp", "@simplysm"]) */
278
- watchScopes?: string[];
279
- /** scope 패키지 dist 변경 감지 시 콜백 */
249
+ /** replaceDeps 패키지명 배열 (resolve 완료된 상태) */
250
+ replaceDeps?: string[];
251
+ /** replaceDeps 패키지 dist 변경 감지 시 콜백 */
280
252
  onScopeRebuild?: () => void;
281
253
  }
282
254
 
@@ -288,7 +260,7 @@ export interface ViteConfigOptions {
288
260
  * - dev 모드: dev server (define으로 env 치환, server 설정)
289
261
  */
290
262
  export function createViteConfig(options: ViteConfigOptions): ViteUserConfig {
291
- const { pkgDir, name, tsconfigPath, compilerOptions, env, mode, serverPort, watchScopes } =
263
+ const { pkgDir, name, tsconfigPath, compilerOptions, env, mode, serverPort, replaceDeps } =
292
264
  options;
293
265
 
294
266
  // Read package.json to extract app name for PWA manifest
@@ -322,11 +294,11 @@ export function createViteConfig(options: ViteConfigOptions): ViteUserConfig {
322
294
  globPatterns: ["**/*.{js,css,html,ico,png,svg,woff2}"],
323
295
  },
324
296
  }),
325
- ...(watchScopes != null && watchScopes.length > 0
326
- ? [sdTailwindConfigDepsPlugin(pkgDir, watchScopes)]
297
+ ...(replaceDeps != null && replaceDeps.length > 0
298
+ ? [sdTailwindConfigDepsPlugin(pkgDir, replaceDeps)]
327
299
  : []),
328
- ...(watchScopes != null && watchScopes.length > 0
329
- ? [sdScopeWatchPlugin(pkgDir, watchScopes, options.onScopeRebuild)]
300
+ ...(replaceDeps != null && replaceDeps.length > 0
301
+ ? [sdScopeWatchPlugin(pkgDir, replaceDeps, options.onScopeRebuild)]
330
302
  : []),
331
303
  ...(mode === "dev" ? [sdPublicDevPlugin(pkgDir)] : []),
332
304
  ],
@@ -6,6 +6,7 @@ import { consola } from "consola";
6
6
  import type { SdClientPackageConfig } from "../sd-config.types";
7
7
  import { parseRootTsconfig, getCompilerOptionsForPackage } from "../utils/tsconfig";
8
8
  import { createViteConfig } from "../utils/vite-config";
9
+ import { collectDeps } from "../utils/package-utils";
9
10
  import { registerCleanupHandlers } from "../utils/worker-utils";
10
11
 
11
12
  //#region Types
@@ -36,8 +37,8 @@ export interface ClientWatchInfo {
36
37
  config: SdClientPackageConfig;
37
38
  cwd: string;
38
39
  pkgDir: string;
39
- /** watch 대상 scope 목록 */
40
- watchScopes?: string[];
40
+ /** sd.config.ts의 replaceDeps 설정 */
41
+ replaceDeps?: Record<string, string>;
41
42
  }
42
43
 
43
44
  /**
@@ -176,6 +177,9 @@ async function startWatch(info: ClientWatchInfo): Promise<void> {
176
177
  // server가 숫자면 해당 포트로 고정 (standalone 클라이언트)
177
178
  const serverPort = typeof info.config.server === "number" ? info.config.server : 0;
178
179
 
180
+ // 의존성 기반 replaceDeps 수집
181
+ const { replaceDeps } = collectDeps(info.pkgDir, info.cwd, info.replaceDeps);
182
+
179
183
  // Vite 설정 생성
180
184
  const viteConfig = createViteConfig({
181
185
  pkgDir: info.pkgDir,
@@ -185,7 +189,7 @@ async function startWatch(info: ClientWatchInfo): Promise<void> {
185
189
  env: info.config.env,
186
190
  mode: "dev",
187
191
  serverPort,
188
- watchScopes: info.watchScopes,
192
+ replaceDeps,
189
193
  onScopeRebuild: () => sender.send("scopeRebuild", {}),
190
194
  });
191
195
 
@@ -2,7 +2,7 @@ import path from "path";
2
2
  import fs from "fs";
3
3
  import cp from "child_process";
4
4
  import esbuild from "esbuild";
5
- import { createWorker, FsWatcher } from "@simplysm/core-node";
5
+ import { createWorker, FsWatcher, pathNorm } from "@simplysm/core-node";
6
6
  import { consola } from "consola";
7
7
  import {
8
8
  parseRootTsconfig,
@@ -13,8 +13,10 @@ import {
13
13
  createServerEsbuildOptions,
14
14
  collectUninstalledOptionalPeerDeps,
15
15
  collectNativeModuleExternals,
16
+ writeChangedOutputFiles,
16
17
  } from "../utils/esbuild-config";
17
18
  import { registerCleanupHandlers } from "../utils/worker-utils";
19
+ import { collectDeps } from "../utils/package-utils";
18
20
  import { copyPublicFiles, watchPublicFiles } from "../utils/copy-public";
19
21
 
20
22
  //#region Types
@@ -64,8 +66,8 @@ export interface ServerWatchInfo {
64
66
  configs?: Record<string, unknown>;
65
67
  /** sd.config.ts에서 수동 지정한 external 모듈 */
66
68
  externals?: string[];
67
- /** scope 패키지 감시 대상 (e.g. ["@simplysm"]) */
68
- watchScopes?: string[];
69
+ /** sd.config.ts의 replaceDeps 설정 */
70
+ replaceDeps?: Record<string, string>;
69
71
  }
70
72
 
71
73
  /**
@@ -103,6 +105,9 @@ const logger = consola.withTag("sd:cli:server:worker");
103
105
  /** esbuild build context (정리 대상) */
104
106
  let esbuildContext: esbuild.BuildContext | undefined;
105
107
 
108
+ /** 마지막 빌드의 metafile (rebuild 시 변경 파일 필터링용) */
109
+ let lastMetafile: esbuild.Metafile | undefined;
110
+
106
111
  /** public 파일 watcher (정리 대상) */
107
112
  let publicWatcher: FsWatcher | undefined;
108
113
 
@@ -117,6 +122,7 @@ async function cleanup(): Promise<void> {
117
122
  // (Promise.all 대기 중 다른 호출에서 전역 변수를 수정할 수 있으므로)
118
123
  const contextToDispose = esbuildContext;
119
124
  esbuildContext = undefined;
125
+ lastMetafile = undefined;
120
126
 
121
127
  const watcherToClose = publicWatcher;
122
128
  publicWatcher = undefined;
@@ -385,6 +391,8 @@ async function createAndBuildContext(
385
391
 
386
392
  const context = await esbuild.context({
387
393
  ...baseOptions,
394
+ metafile: true,
395
+ write: false,
388
396
  plugins: [
389
397
  {
390
398
  name: "watch-notify",
@@ -393,22 +401,38 @@ async function createAndBuildContext(
393
401
  sender.send("buildStart", {});
394
402
  });
395
403
 
396
- pluginBuild.onEnd((result) => {
404
+ pluginBuild.onEnd(async (result) => {
405
+ // metafile 저장
406
+ if (result.metafile != null) {
407
+ lastMetafile = result.metafile;
408
+ }
409
+
397
410
  const errors = result.errors.map((e) => e.text);
398
411
  const warnings = result.warnings.map((w) => w.text);
399
412
  const success = result.errors.length === 0;
400
413
 
414
+ // output 파일 쓰기 및 변경 여부 확인
415
+ let hasOutputChange = false;
416
+ if (success && result.outputFiles != null) {
417
+ hasOutputChange = await writeChangedOutputFiles(result.outputFiles);
418
+ }
419
+
401
420
  if (isBuildFirstTime && success) {
402
421
  const confDistPath = path.join(info.pkgDir, "dist", ".config.json");
403
422
  fs.writeFileSync(confDistPath, JSON.stringify(info.configs ?? {}, undefined, 2));
404
423
  }
405
424
 
406
- sender.send("build", {
407
- success,
408
- mainJsPath,
409
- errors: errors.length > 0 ? errors : undefined,
410
- warnings: warnings.length > 0 ? warnings : undefined,
411
- });
425
+ // 첫 빌드이거나, output이 변경되었거나, 에러인 경우에만 build 이벤트 발생
426
+ if (isBuildFirstTime || hasOutputChange || !success) {
427
+ sender.send("build", {
428
+ success,
429
+ mainJsPath,
430
+ errors: errors.length > 0 ? errors : undefined,
431
+ warnings: warnings.length > 0 ? warnings : undefined,
432
+ });
433
+ } else {
434
+ logger.debug("output 변경 없음, 서버 재시작 skip");
435
+ }
412
436
 
413
437
  if (isBuildFirstTime) {
414
438
  isBuildFirstTime = false;
@@ -452,25 +476,27 @@ async function startWatch(info: ServerWatchInfo): Promise<void> {
452
476
  // Watch public/ and public-dev/ (dev mode includes public-dev)
453
477
  publicWatcher = await watchPublicFiles(info.pkgDir, true);
454
478
 
455
- // FsWatcher 감시 경로 수집
456
- const watchPaths: string[] = [];
479
+ // 의존성 기반 감시 경로 수집
480
+ const { workspaceDeps, replaceDeps } = collectDeps(info.pkgDir, info.cwd, info.replaceDeps);
457
481
 
458
- // 1) 서버 자체 소스
459
- watchPaths.push(path.join(info.pkgDir, "src", "**", "*.{ts,tsx}"));
482
+ const watchPaths: string[] = [];
460
483
 
461
- // 2) scope 패키지 dist 디렉토리
462
- if (info.watchScopes != null) {
463
- for (const scope of info.watchScopes) {
464
- const scopeDir = path.join(info.pkgDir, "node_modules", scope);
465
- if (!fs.existsSync(scopeDir)) continue;
484
+ // 1) 서버 패키지 자신 + workspace 의존 패키지 소스
485
+ const watchDirs = [
486
+ info.pkgDir,
487
+ ...workspaceDeps.map((d) => path.join(info.cwd, "packages", d)),
488
+ ];
489
+ for (const dir of watchDirs) {
490
+ watchPaths.push(path.join(dir, "src", "**", "*"));
491
+ watchPaths.push(path.join(dir, "*.{ts,js,css}"));
492
+ }
466
493
 
467
- for (const pkgName of fs.readdirSync(scopeDir)) {
468
- const distDir = path.join(scopeDir, pkgName, "dist");
469
- if (fs.existsSync(distDir)) {
470
- watchPaths.push(distDir);
471
- }
472
- }
473
- }
494
+ // 2) replaceDeps 의존 패키지 dist (루트 + 패키지 node_modules)
495
+ for (const pkg of replaceDeps) {
496
+ watchPaths.push(path.join(info.cwd, "node_modules", ...pkg.split("/"), "dist", "**", "*.js"));
497
+ watchPaths.push(
498
+ path.join(info.pkgDir, "node_modules", ...pkg.split("/"), "dist", "**", "*.js"),
499
+ );
474
500
  }
475
501
 
476
502
  // FsWatcher 시작
@@ -479,16 +505,11 @@ async function startWatch(info: ServerWatchInfo): Promise<void> {
479
505
  // 파일 변경 감지 시 처리
480
506
  srcWatcher.onChange({ delay: 300 }, async (changes) => {
481
507
  try {
482
- // 서버 자체 소스에서 add/unlink가 있으면 context 재생성
483
- const srcDir = path.join(info.pkgDir, "src");
484
- const hasEntryPointChange = changes.some(
485
- (c) =>
486
- (c.event === "add" || c.event === "unlink") &&
487
- c.path.startsWith(srcDir.replace(/\\/g, "/")),
488
- );
508
+ // 파일 추가/삭제가 있으면 context 재생성 (import graph 변경 가능)
509
+ const hasFileAddOrRemove = changes.some((c) => c.event === "add" || c.event === "unlink");
489
510
 
490
- if (hasEntryPointChange) {
491
- logger.debug("서버 소스 파일 추가/삭제 감지, context 재생성");
511
+ if (hasFileAddOrRemove) {
512
+ logger.debug("파일 추가/삭제 감지, context 재생성");
492
513
 
493
514
  const oldContext = esbuildContext;
494
515
  esbuildContext = await createAndBuildContext(info, false);
@@ -496,10 +517,29 @@ async function startWatch(info: ServerWatchInfo): Promise<void> {
496
517
  if (oldContext != null) {
497
518
  await oldContext.dispose();
498
519
  }
520
+ return;
521
+ }
522
+
523
+ // 파일 변경만 있는 경우: metafile 필터링
524
+ if (esbuildContext == null) return;
525
+
526
+ // metafile이 없으면 (첫 빌드 전) 무조건 rebuild
527
+ if (lastMetafile == null) {
528
+ await esbuildContext.rebuild();
529
+ return;
530
+ }
531
+
532
+ // metafile.inputs 키를 절대경로(NormPath)로 변환하여 비교
533
+ const metafileAbsPaths = new Set(
534
+ Object.keys(lastMetafile.inputs).map((key) => pathNorm(info.cwd, key)),
535
+ );
536
+
537
+ const hasRelevantChange = changes.some((c) => metafileAbsPaths.has(c.path));
538
+
539
+ if (hasRelevantChange) {
540
+ await esbuildContext.rebuild();
499
541
  } else {
500
- if (esbuildContext != null) {
501
- await esbuildContext.rebuild();
502
- }
542
+ logger.debug("변경된 파일이 빌드에 포함되지 않음, rebuild skip");
503
543
  }
504
544
  } catch (err) {
505
545
  sender.send("error", {
@@ -4,7 +4,7 @@
4
4
  "type": "module",
5
5
  "private": true,
6
6
  "dependencies": {
7
- "@simplysm/solid": "~13.0.64",
7
+ "@simplysm/solid": "~13.0.66",
8
8
  {{#if router}}
9
9
  "@solidjs/router": "^0.15.4",
10
10
  {{/if}}
@@ -4,7 +4,7 @@
4
4
  "type": "module",
5
5
  "private": true,
6
6
  "dependencies": {
7
- "@simplysm/core-common": "~13.0.64",
8
- "@simplysm/service-server": "~13.0.64"
7
+ "@simplysm/core-common": "~13.0.66",
8
+ "@simplysm/service-server": "~13.0.66"
9
9
  }
10
10
  }
@@ -15,9 +15,9 @@
15
15
  "vitest": "vitest"
16
16
  },
17
17
  "devDependencies": {
18
- "@simplysm/sd-cli": "~13.0.64",
19
- "@simplysm/sd-claude": "~13.0.64",
20
- "@simplysm/lint": "~13.0.64",
18
+ "@simplysm/sd-cli": "~13.0.66",
19
+ "@simplysm/sd-claude": "~13.0.66",
20
+ "@simplysm/lint": "~13.0.66",
21
21
  "@types/node": "^20.19.33",
22
22
  "eslint": "^9.39.2",
23
23
  "prettier": "^3.8.1",