@simplysm/sd-cli 14.0.38 → 14.0.40

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 (57) hide show
  1. package/dist/angular/angular-build-pipeline.d.ts +1 -1
  2. package/dist/angular/angular-build-pipeline.js +1 -1
  3. package/dist/angular/client-transform-stylesheet.d.ts +1 -1
  4. package/dist/angular/client-transform-stylesheet.js +3 -3
  5. package/dist/dev-server/hmr-client-script.d.ts +2 -2
  6. package/dist/dev-server/hmr-client-script.d.ts.map +1 -1
  7. package/dist/dev-server/hmr-client-script.js +4 -4
  8. package/dist/dev-server/hmr-client-script.js.map +1 -1
  9. package/dist/esbuild/esbuild-client-config.d.ts +0 -2
  10. package/dist/esbuild/esbuild-client-config.d.ts.map +1 -1
  11. package/dist/esbuild/esbuild-client-config.js +20 -10
  12. package/dist/esbuild/esbuild-client-config.js.map +1 -1
  13. package/dist/esbuild/esbuild-postcss-plugin.d.ts +8 -0
  14. package/dist/esbuild/esbuild-postcss-plugin.d.ts.map +1 -0
  15. package/dist/esbuild/esbuild-postcss-plugin.js +105 -0
  16. package/dist/esbuild/esbuild-postcss-plugin.js.map +1 -0
  17. package/dist/esbuild/esbuild-tsc-plugin.d.ts +23 -0
  18. package/dist/esbuild/esbuild-tsc-plugin.d.ts.map +1 -0
  19. package/dist/esbuild/esbuild-tsc-plugin.js +60 -0
  20. package/dist/esbuild/esbuild-tsc-plugin.js.map +1 -0
  21. package/dist/workers/client.worker.d.ts.map +1 -1
  22. package/dist/workers/client.worker.js +94 -26
  23. package/dist/workers/client.worker.js.map +1 -1
  24. package/dist/workers/server-build.worker.d.ts.map +1 -1
  25. package/dist/workers/server-build.worker.js +129 -90
  26. package/dist/workers/server-build.worker.js.map +1 -1
  27. package/dist/workers/server-esbuild-context.d.ts +27 -0
  28. package/dist/workers/server-esbuild-context.d.ts.map +1 -1
  29. package/dist/workers/server-esbuild-context.js +57 -4
  30. package/dist/workers/server-esbuild-context.js.map +1 -1
  31. package/package.json +6 -4
  32. package/src/angular/angular-build-pipeline.ts +2 -2
  33. package/src/angular/client-transform-stylesheet.ts +4 -4
  34. package/src/dev-server/hmr-client-script.ts +4 -4
  35. package/src/esbuild/esbuild-client-config.ts +22 -13
  36. package/src/esbuild/esbuild-postcss-plugin.ts +117 -0
  37. package/src/esbuild/esbuild-tsc-plugin.ts +83 -0
  38. package/src/workers/client.worker.ts +96 -29
  39. package/src/workers/server-build.worker.ts +136 -97
  40. package/src/workers/server-esbuild-context.ts +72 -4
  41. package/tests/angular/client-transform-stylesheet.spec.ts +1 -1
  42. package/tests/esbuild/esbuild-tsc-plugin.acc.spec.ts +349 -0
  43. package/tests/esbuild/esbuild-tsc-plugin.spec.ts +230 -0
  44. package/tests/utils/esbuild-client-config-postcss.verify.md +6 -0
  45. package/tests/utils/esbuild-client-config.acc.spec.ts +34 -20
  46. package/tests/utils/esbuild-client-config.spec.ts +79 -16
  47. package/tests/utils/esbuild-postcss-plugin.acc.spec.ts +299 -0
  48. package/tests/utils/esbuild-postcss-plugin.spec.ts +290 -0
  49. package/tests/utils/esbuild-scss-plugin.acc.spec.ts +1 -0
  50. package/tests/utils/hmr-client-script.acc.spec.ts +8 -8
  51. package/tests/utils/hmr-client-script.spec.ts +5 -5
  52. package/tests/workers/server-build-lint.spec.ts +43 -0
  53. package/tests/workers/server-build-worker-refactoring.verify.md +14 -0
  54. package/tests/workers/server-build-worker.spec.ts +122 -9
  55. package/tests/workers/server-esbuild-context-tsc.verify.md +7 -0
  56. package/tests/workers/server-esbuild-context.acc.spec.ts +188 -2
  57. package/tests/workers/server-esbuild-context.spec.ts +401 -2
@@ -1,6 +1,6 @@
1
1
  import path from "path";
2
2
  import fs from "node:fs";
3
- import { createWorker } from "@simplysm/core-node";
3
+ import { createWorker, FsWatcher } from "@simplysm/core-node";
4
4
  import { err as errNs } from "@simplysm/core-common";
5
5
  import { setupWorkerLifecycle } from "./shared-worker-lifecycle.js";
6
6
  import {
@@ -14,7 +14,6 @@ import { createHmrService, type HmrService } from "../dev-server/hmr-service.js"
14
14
  import { createHmrPostTransform } from "../dev-server/hmr-client-script.js";
15
15
  import { copyPublicFiles, watchPublicFiles } from "../utils/copy-public.js";
16
16
  import type { SdBrowserSupportConfig, SdPwaConfig } from "../sd-config.types.js";
17
- import type { FsWatcher } from "@simplysm/core-node";
18
17
  import type esbuild from "esbuild";
19
18
 
20
19
  //#region Types
@@ -63,6 +62,7 @@ let esbuildResult: ClientEsbuildResult | undefined;
63
62
  let devServer: DevHttpServer | undefined;
64
63
  let hmrService: HmrService | undefined;
65
64
  let publicWatcher: FsWatcher | undefined;
65
+ let indexHtmlWatcher: FsWatcher | undefined;
66
66
 
67
67
  const { logger, guardStartWatch } = setupWorkerLifecycle("client", async () => {
68
68
  await stopWatch();
@@ -118,7 +118,6 @@ async function build(info: ClientBuildInfo): Promise<ClientBuildResult> {
118
118
  polyfills,
119
119
  legacyModule,
120
120
  postcssPlugins,
121
- postcssConfigPath: info.pkgDir,
122
121
  templateUpdates,
123
122
  });
124
123
 
@@ -211,8 +210,29 @@ async function startWatch(info: ClientBuildInfo): Promise<ClientBuildResult> {
211
210
  const polyfills = fs.existsSync(polyfillsPath) ? ["src/polyfills.ts"] : undefined;
212
211
  const entryNames = ["main", ...(polyfills != null ? ["polyfills"] : [])];
213
212
 
214
- // 4. templateUpdates Map + esbuild context 생성
213
+ // 4. templateUpdates Map
215
214
  const templateUpdates = new Map<string, string>();
215
+
216
+ // 5. HTTP dev server 생성 + 시작 (포트 확정 — HMR 스크립트에 포트 주입 필요)
217
+ const httpDevServer = createDevHttpServer({
218
+ distDir: outdir,
219
+ basePath,
220
+ port: info.port ?? 0,
221
+ onRequest: (req, res) => hmrService?.handleRequest(req, res) ?? false,
222
+ });
223
+ devServer = httpDevServer;
224
+ const actualPort = await httpDevServer.listen();
225
+
226
+ // 6. HMR WebSocket 서비스 생성
227
+ hmrService = createHmrService({
228
+ httpServer: httpDevServer.httpServer,
229
+ basePath,
230
+ templateUpdates,
231
+ outDir: outdir,
232
+ });
233
+
234
+ // 7. esbuild context 생성
235
+ let lastMetafile: esbuild.Metafile | undefined;
216
236
  let initialBuildResolve: ((result: ClientBuildResult) => void) | undefined;
217
237
  let isInitialBuild = true;
218
238
 
@@ -226,25 +246,61 @@ async function startWatch(info: ClientBuildInfo): Promise<ClientBuildResult> {
226
246
  polyfills,
227
247
  legacyModule,
228
248
  postcssPlugins,
229
- postcssConfigPath: info.pkgDir,
230
249
  templateUpdates: legacyModule ? undefined : templateUpdates,
231
250
  plugins: [
232
251
  {
233
252
  name: "sd-build-start",
234
253
  setup(pluginBuild: esbuild.PluginBuild) {
254
+ const prevMtimes = new Map<string, number>();
255
+
235
256
  pluginBuild.onStart(() => {
257
+ // sourceFileCache 무효화: 변경된 파일의 loadResultCache + TypeScript 소스 캐시 모두 제거
258
+ if (esbuildResult != null) {
259
+ const { loadResultCache } = esbuildResult.sourceFileCache;
260
+ const changedFiles = new Set<string>();
261
+ for (const file of loadResultCache.watchFiles) {
262
+ try {
263
+ const mtime = fs.statSync(file).mtimeMs;
264
+ const prev = prevMtimes.get(file);
265
+ if (prev != null && prev !== mtime) {
266
+ changedFiles.add(file);
267
+ }
268
+ } catch {
269
+ if (prevMtimes.has(file)) {
270
+ changedFiles.add(file);
271
+ }
272
+ }
273
+ }
274
+ if (changedFiles.size > 0) {
275
+ esbuildResult.sourceFileCache.invalidate(changedFiles);
276
+ }
277
+ }
278
+
236
279
  if (!isInitialBuild) {
237
280
  sender.send("buildStart", {});
238
281
  }
239
282
  });
283
+
284
+ pluginBuild.onEnd(() => {
285
+ if (esbuildResult == null) return;
286
+ prevMtimes.clear();
287
+ for (const file of esbuildResult.sourceFileCache.loadResultCache.watchFiles) {
288
+ try {
289
+ prevMtimes.set(file, fs.statSync(file).mtimeMs);
290
+ } catch {
291
+ // 삭제된 파일
292
+ }
293
+ }
294
+ });
240
295
  },
241
296
  },
242
297
  ],
243
298
  onEnd: async (result: esbuild.BuildResult) => {
244
299
  try {
245
- // index.html 재생성
300
+ // index.html 재생성 (lastMetafile 보관 — index.html 단독 변경 시 재생성용)
246
301
  if (result.metafile != null) {
247
- const hmrPostTransform = createHmrPostTransform(basePath);
302
+ lastMetafile = result.metafile;
303
+ const hmrPostTransform = createHmrPostTransform(basePath, actualPort);
248
304
  const indexPath = path.join(info.pkgDir, "src", "index.html");
249
305
  const indexResult = await generateIndexHtml({
250
306
  indexPath,
@@ -301,37 +357,42 @@ async function startWatch(info: ClientBuildInfo): Promise<ClientBuildResult> {
301
357
  },
302
358
  });
303
359
 
304
- // 5. HTTP dev server 생성
305
- const httpDevServer = createDevHttpServer({
306
- distDir: outdir,
307
- basePath,
308
- port: info.port ?? 0,
309
- onRequest: (req, res) => hmrService?.handleRequest(req, res) ?? false,
310
- });
311
- devServer = httpDevServer;
312
-
313
- // 6. HMR WebSocket 서비스 생성
314
- hmrService = createHmrService({
315
- httpServer: httpDevServer.httpServer,
316
- basePath,
317
- templateUpdates,
318
- outDir: outdir,
319
- });
320
-
321
- // 7. esbuild watch 시작 + 초기 빌드 대기
360
+ // 8. esbuild watch 시작 + 초기 빌드 대기
322
361
  await esbuildResult.context.watch();
323
362
 
324
363
  const initialResult = await new Promise<ClientBuildResult>((resolve) => {
325
364
  initialBuildResolve = resolve;
326
365
  });
327
366
 
328
- // 8. HTTP 서버 시작
329
- const actualPort = await httpDevServer.listen();
367
+ // 9. src/index.html 감시 (esbuild watch는 HTML을 감시하지 않음)
368
+ const indexHtmlSrcPath = path.join(info.pkgDir, "src", "index.html");
369
+ indexHtmlWatcher = await FsWatcher.watch([indexHtmlSrcPath]);
370
+ indexHtmlWatcher.onChange({ delay: 300 }, async () => {
371
+ if (lastMetafile == null) return;
372
+ try {
373
+ sender.send("buildStart", {});
374
+ const hmrPostTransform = createHmrPostTransform(basePath, actualPort);
375
+ const indexResult = await generateIndexHtml({
376
+ indexPath: indexHtmlSrcPath,
377
+ metafile: lastMetafile,
378
+ outdir,
379
+ baseHref: basePath,
380
+ mode: "dev",
381
+ entryNames,
382
+ postTransform: hmrPostTransform,
383
+ });
384
+ fs.writeFileSync(path.join(outdir, "index.html"), indexResult.content);
385
+ hmrService?.broadcast({ type: "full-reload" });
386
+ sender.send("build", { success: true });
387
+ } catch (err) {
388
+ sender.send("error", { message: errNs.message(err) });
389
+ }
390
+ });
330
391
 
331
- // 9. serverReady 이벤트 전송
392
+ // 10. serverReady 이벤트 전송
332
393
  sender.send("serverReady", { port: actualPort });
333
394
 
334
- // 10. .config.json + .dev-port 기록
395
+ // 11. .config.json + .dev-port 기록
335
396
  writeConfigJson(outdir, info.configs);
336
397
  fs.writeFileSync(path.join(outdir, ".dev-port"), String(actualPort));
337
398
 
@@ -373,6 +434,12 @@ async function stopWatch(): Promise<void> {
373
434
  publicWatcher = undefined;
374
435
  }
375
436
 
437
+ // 5. index.html 감시 종료
438
+ if (indexHtmlWatcher != null) {
439
+ await indexHtmlWatcher.close();
440
+ indexHtmlWatcher = undefined;
441
+ }
442
+
376
443
  logger.debug("esbuild watch 정리 완료");
377
444
  }
378
445
 
@@ -17,6 +17,7 @@ import {
17
17
  } from "../esbuild/esbuild-config";
18
18
  import { collectAllExternals, generateProductionFiles } from "../deps/server-externals/server-production-files";
19
19
  import { runTscPackageBuild } from "../utils/tsc-build";
20
+ import { createTscPlugin } from "../esbuild/esbuild-tsc-plugin";
20
21
  import { LintWithProgramRunner } from "../lint/lint-with-program";
21
22
  import { setupWorkerLifecycle } from "./shared-worker-lifecycle";
22
23
  import { buildWatchPaths } from "./build-watch-paths";
@@ -106,7 +107,6 @@ let srcWatcher: FsWatcher | undefined;
106
107
 
107
108
  async function cleanup(): Promise<void> {
108
109
  await esbuildCtx.dispose();
109
- lastBuilderProgram = undefined;
110
110
 
111
111
  const watcherToClose = publicWatcher;
112
112
  publicWatcher = undefined;
@@ -143,16 +143,30 @@ async function build(info: ServerBuildInfo): Promise<ServerBuildResult> {
143
143
  // 외부 모듈 수집
144
144
  const external = collectAllExternals(info.pkgDir, info.externals);
145
145
 
146
- // esbuild (비동기) tsc (동기) 병렬 실행
147
- const esbuildOptions = createServerEsbuildOptions({
148
- pkgDir: info.pkgDir,
149
- entryPoints,
150
- env: info.env,
151
- external,
152
- });
146
+ let jsResult: { success: boolean; errors?: string[]; warnings?: string[] };
147
+ let tscErrors: string[];
148
+ let tscDiagnostics: SerializedDiagnostic[];
149
+ let tscProgram: ts.Program | undefined;
150
+
151
+ if (info.output.js) {
152
+ // js=true: tsc 플러그인 통합 — 단일 esbuild.build() 호출
153
+ const tscPlugin = createTscPlugin({
154
+ pkgDir: info.pkgDir,
155
+ cwd: info.cwd,
156
+ output: { dts: info.output.dts },
157
+ env: info.output.env,
158
+ includeTests: info.output.includeTests,
159
+ });
153
160
 
154
- const esbuildPromise = info.output.js
155
- ? esbuild.build(esbuildOptions).then(async (result) => {
161
+ const esbuildOptions = createServerEsbuildOptions({
162
+ pkgDir: info.pkgDir,
163
+ entryPoints,
164
+ env: info.env,
165
+ external,
166
+ });
167
+
168
+ jsResult = await esbuild.build({ ...esbuildOptions, plugins: [tscPlugin.plugin] })
169
+ .then(async (result) => {
156
170
  if (result.outputFiles) {
157
171
  await writeChangedOutputFiles(result.outputFiles);
158
172
  }
@@ -163,36 +177,42 @@ async function build(info: ServerBuildInfo): Promise<ServerBuildResult> {
163
177
  errors: errors.length > 0 ? errors : undefined,
164
178
  warnings: warnings.length > 0 ? warnings : undefined,
165
179
  };
166
- }).catch((err) => ({
180
+ })
181
+ .catch((err) => ({
167
182
  success: false,
168
183
  errors: [errNs.message(err)],
169
184
  warnings: undefined,
170
- }))
171
- : null;
172
-
173
- // tsc 타입체크 (항상 실행, emit은 output.dts로 제어)
174
- const tscResult = runTscPackageBuild({
175
- pkgDir: info.pkgDir,
176
- cwd: info.cwd,
177
- output: { js: false, dts: info.output.dts },
178
- parsedConfig,
179
- env: info.output.env,
180
- includeTests: info.output.includeTests,
181
- });
185
+ }));
186
+
187
+ tscErrors = tscPlugin.getErrors() ?? [];
188
+ tscDiagnostics = tscPlugin.getDiagnostics();
189
+ tscProgram = tscPlugin.getProgram();
190
+ } else {
191
+ // js=false: runTscPackageBuild 직접 호출 (플러그인 경유 불가)
192
+ const tscResult = runTscPackageBuild({
193
+ pkgDir: info.pkgDir,
194
+ cwd: info.cwd,
195
+ output: { js: false, dts: info.output.dts },
196
+ parsedConfig,
197
+ env: info.output.env,
198
+ includeTests: info.output.includeTests,
199
+ });
182
200
 
183
- const jsResult = esbuildPromise
184
- ? await esbuildPromise
185
- : { success: true, errors: undefined, warnings: undefined };
201
+ jsResult = { success: true, errors: undefined, warnings: undefined };
202
+ tscErrors = tscResult.errors ?? [];
203
+ tscDiagnostics = tscResult.diagnostics;
204
+ tscProgram = tscResult.program;
205
+ }
186
206
 
187
207
  // lint 실행 (활성화 + program 사용 가능 시)
188
208
  let lint: LintWithProgramResult | undefined;
189
- if (info.output.lint === true && tscResult.program != null) {
209
+ if (info.output.lint === true && tscProgram != null) {
190
210
  logger.debug(`[${info.name}] lint 시작`);
191
211
  const lintRunner = new LintWithProgramRunner({
192
212
  cwd: info.cwd,
193
213
  pkgName: info.name,
194
214
  });
195
- lint = await lintRunner.lint({ program: tscResult.program });
215
+ lint = await lintRunner.lint({ program: tscProgram });
196
216
  logger.debug(`[${info.name}] lint 완료`);
197
217
  }
198
218
 
@@ -206,14 +226,15 @@ async function build(info: ServerBuildInfo): Promise<ServerBuildResult> {
206
226
  generateProductionFiles(info, external);
207
227
  }
208
228
 
209
- const allErrors = [...(jsResult.errors ?? []), ...(tscResult.errors ?? [])];
210
- logger.debug(`[${info.name}] server worker build 완료 (js: ${jsResult.success}, tsc: ${tscResult.success})`);
229
+ const allErrors = [...(jsResult.errors ?? []), ...tscErrors];
230
+ const tscSuccess = tscErrors.length === 0;
231
+ logger.debug(`[${info.name}] server worker build 완료 (js: ${jsResult.success}, tsc: ${tscSuccess})`);
211
232
  return {
212
233
  build: {
213
- success: jsResult.success && tscResult.success,
234
+ success: jsResult.success && tscSuccess,
214
235
  errors: allErrors.length > 0 ? allErrors : undefined,
215
236
  warnings: jsResult.warnings,
216
- diagnostics: tscResult.diagnostics,
237
+ diagnostics: tscDiagnostics,
217
238
  },
218
239
  lint,
219
240
  mainJsPath,
@@ -232,76 +253,88 @@ async function build(info: ServerBuildInfo): Promise<ServerBuildResult> {
232
253
  }
233
254
  }
234
255
 
235
- // watch 모드용 가변 상태
236
- let watchInfo: ServerWatchInfo | undefined;
237
- let watchLintRunner: LintWithProgramRunner | undefined;
238
- let lastBuilderProgram: ts.EmitAndSemanticDiagnosticsBuilderProgram | undefined;
239
-
240
256
  /**
241
- * esbuild + tsc 병렬 리빌드 (watch 모드)
257
+ * watch 모드 시작
242
258
  */
243
- async function rebuildAll(): Promise<ServerCombinedBuildEvent> {
244
- const info = watchInfo!;
245
- logger.debug(`[${info.name}] rebuildAll 시작`);
246
- const mainJsPath = pathx.posixResolve(info.pkgDir, "dist", "main.js");
247
- const parsedConfig = parseTsconfig(info.pkgDir);
248
-
249
- // esbuild 리빌드 (비동기)
250
- const esbuildPromise = info.output.js ? esbuildCtx.rebuild() : null;
251
-
252
- // tsc 리빌드 (동기, 증분)
253
- const tscResult = runTscPackageBuild({
254
- pkgDir: info.pkgDir,
255
- cwd: info.cwd,
256
- output: { js: false, dts: info.output.dts },
257
- parsedConfig,
258
- env: info.output.env,
259
- includeTests: info.output.includeTests,
260
- oldBuilderProgram: lastBuilderProgram,
261
- });
262
- lastBuilderProgram = tscResult.builderProgram ?? lastBuilderProgram;
263
-
264
- // lint 실행 (활성화 + program 사용 가능 시)
265
- let lint: LintWithProgramResult | undefined;
266
- if (info.output.lint === true && tscResult.program != null) {
267
- logger.debug(`[${info.name}] lint 시작`);
268
- if (watchLintRunner == null) {
269
- watchLintRunner = new LintWithProgramRunner({
259
+ async function startWatch(info: ServerWatchInfo): Promise<void> {
260
+ guardStartWatch();
261
+ logger.debug(`[${info.name}] server worker startWatch 시작`);
262
+
263
+ // watch 모드 로컬 상태 (클로저로 관리)
264
+ let lastBuilderProgram: ts.EmitAndSemanticDiagnosticsBuilderProgram | undefined;
265
+ let watchLintRunner: LintWithProgramRunner | undefined;
266
+
267
+ /**
268
+ * esbuild + tsc 리빌드 (watch 모드)
269
+ * js=true: esbuildCtx.rebuild() 단일 호출 (tsc 플러그인 자동 트리거)
270
+ * js=false: runTscPackageBuild 직접 호출
271
+ */
272
+ async function rebuildAll(): Promise<ServerCombinedBuildEvent> {
273
+ logger.debug(`[${info.name}] rebuildAll 시작`);
274
+ const mainJsPath = pathx.posixResolve(info.pkgDir, "dist", "main.js");
275
+
276
+ let jsResult: { success: boolean; errors?: string[]; warnings?: string[] };
277
+ let tscProgram: ts.Program | undefined;
278
+ let tscAffectedFiles: ReadonlySet<string> | undefined;
279
+ let tscErrors: string[];
280
+
281
+ if (info.output.js) {
282
+ // js=true: esbuildCtx.rebuild() 단일 호출 (tsc 에러 자동 병합)
283
+ const rebuildResult = await esbuildCtx.rebuild();
284
+ jsResult = rebuildResult ?? { success: true, errors: undefined, warnings: undefined };
285
+ tscProgram = esbuildCtx.getTscProgram();
286
+ tscAffectedFiles = esbuildCtx.getTscAffectedFiles();
287
+ tscErrors = [];
288
+ } else {
289
+ // js=false: runTscPackageBuild 직접 호출
290
+ const parsedConfig = parseTsconfig(info.pkgDir);
291
+ const tscResult = runTscPackageBuild({
292
+ pkgDir: info.pkgDir,
270
293
  cwd: info.cwd,
271
- pkgName: info.name,
294
+ output: { js: false, dts: info.output.dts },
295
+ parsedConfig,
296
+ env: info.output.env,
297
+ includeTests: info.output.includeTests,
298
+ oldBuilderProgram: lastBuilderProgram,
272
299
  });
300
+ lastBuilderProgram = tscResult.builderProgram ?? lastBuilderProgram;
301
+
302
+ jsResult = { success: true, errors: undefined, warnings: undefined };
303
+ tscProgram = tscResult.program;
304
+ tscAffectedFiles = tscResult.affectedFiles;
305
+ tscErrors = tscResult.errors ?? [];
273
306
  }
274
- lint = await watchLintRunner.lint({
275
- program: tscResult.program,
276
- affectedFiles: tscResult.affectedFiles,
277
- });
278
- logger.debug(`[${info.name}] lint 완료`);
279
- }
280
307
 
281
- const jsResult = esbuildPromise != null
282
- ? (await esbuildPromise) ?? { success: true, errors: undefined, warnings: undefined }
283
- : { success: true, errors: undefined, warnings: undefined };
284
-
285
- const allErrors = [...(jsResult.errors ?? []), ...(tscResult.errors ?? [])];
286
- logger.debug(`[${info.name}] rebuildAll 완료`);
287
- return {
288
- build: {
289
- success: jsResult.success && tscResult.success,
290
- errors: allErrors.length > 0 ? allErrors : undefined,
291
- warnings: jsResult.warnings,
292
- },
293
- lint,
294
- mainJsPath,
295
- };
296
- }
308
+ // lint 실행 (활성화 + program 사용 가능 시)
309
+ let lint: LintWithProgramResult | undefined;
310
+ if (info.output.lint === true && tscProgram != null) {
311
+ logger.debug(`[${info.name}] lint 시작`);
312
+ if (watchLintRunner == null) {
313
+ watchLintRunner = new LintWithProgramRunner({
314
+ cwd: info.cwd,
315
+ pkgName: info.name,
316
+ });
317
+ }
318
+ lint = await watchLintRunner.lint({
319
+ program: tscProgram,
320
+ affectedFiles: tscAffectedFiles,
321
+ });
322
+ logger.debug(`[${info.name}] lint 완료`);
323
+ }
297
324
 
298
- /**
299
- * watch 모드 시작
300
- */
301
- async function startWatch(info: ServerWatchInfo): Promise<void> {
302
- guardStartWatch();
303
- watchInfo = info;
304
- logger.debug(`[${info.name}] server worker startWatch 시작`);
325
+ const allErrors = [...(jsResult.errors ?? []), ...tscErrors];
326
+ const allSuccess = jsResult.success && tscErrors.length === 0;
327
+ logger.debug(`[${info.name}] rebuildAll 완료`);
328
+ return {
329
+ build: {
330
+ success: allSuccess,
331
+ errors: allErrors.length > 0 ? allErrors : undefined,
332
+ warnings: jsResult.warnings,
333
+ },
334
+ lint,
335
+ mainJsPath,
336
+ };
337
+ }
305
338
 
306
339
  try {
307
340
  const parsedConfig = parseTsconfig(info.pkgDir);
@@ -310,17 +343,23 @@ async function startWatch(info: ServerWatchInfo): Promise<void> {
310
343
  // 외부 모듈 수집 (watch 모드용 — watch manager가 자체 캐시를 유지)
311
344
  const cachedExternal = collectAllExternals(info.pkgDir, info.externals);
312
345
 
313
- // esbuild 컨텍스트 생성 (JS 출력 필요 )
346
+ // esbuild 컨텍스트 생성 (JS 출력 필요 시, tsc 플러그인 포함)
314
347
  if (info.output.js) {
315
348
  await esbuildCtx.createContext({
316
349
  pkgDir: info.pkgDir,
317
350
  entryPoints,
318
351
  env: info.env,
319
352
  external: cachedExternal,
353
+ tsc: {
354
+ cwd: info.cwd,
355
+ output: { dts: info.output.dts },
356
+ env: info.output.env,
357
+ includeTests: info.output.includeTests,
358
+ },
320
359
  });
321
360
  }
322
361
 
323
- // 초기 빌드: esbuild + tsc 병렬
362
+ // 초기 빌드
324
363
  sender.send("buildStart", {});
325
364
  const initialResult = await rebuildAll();
326
365
 
@@ -1,8 +1,13 @@
1
+ import type ts from "typescript";
1
2
  import esbuild from "esbuild";
3
+ import { err as errNs } from "@simplysm/core-common";
2
4
  import {
3
5
  createServerEsbuildOptions,
4
6
  writeChangedOutputFiles,
5
7
  } from "../esbuild/esbuild-config";
8
+ import { createTscPlugin, type TscPluginResult } from "../esbuild/esbuild-tsc-plugin";
9
+ import type { TypecheckEnv } from "../utils/tsconfig";
10
+ import type { SerializedDiagnostic } from "../typecheck/typecheck-serialization";
6
11
 
7
12
  /**
8
13
  * esbuild watch context 생성 옵션
@@ -12,6 +17,13 @@ export interface EsbuildContextOptions {
12
17
  entryPoints: string[];
13
18
  env?: Record<string, string>;
14
19
  external: string[];
20
+ /** tsc 플러그인 옵션. 제공 시 createTscPlugin으로 플러그인을 생성하여 esbuild context에 포함한다. */
21
+ tsc?: {
22
+ cwd: string;
23
+ output: { dts: boolean };
24
+ env?: TypecheckEnv;
25
+ includeTests?: boolean;
26
+ };
15
27
  }
16
28
 
17
29
  /** esbuild watch context (모듈 스코프 상태) */
@@ -20,11 +32,24 @@ let context: esbuild.BuildContext | undefined;
20
32
  /** 마지막 빌드의 metafile (변경 필터링용) */
21
33
  let lastMetafile: esbuild.Metafile | undefined;
22
34
 
35
+ /** tsc 플러그인 인스턴스 (모듈 스코프 상태) */
36
+ let tscPlugin: TscPluginResult | undefined;
37
+
23
38
  /**
24
39
  * esbuild watch context를 생성한다.
25
40
  * dev 모드 전용 (metafile:true, write:false).
26
41
  */
27
42
  export async function createContext(options: EsbuildContextOptions): Promise<void> {
43
+ if (options.tsc != null) {
44
+ tscPlugin = createTscPlugin({
45
+ pkgDir: options.pkgDir,
46
+ cwd: options.tsc.cwd,
47
+ output: options.tsc.output,
48
+ env: options.tsc.env,
49
+ includeTests: options.tsc.includeTests,
50
+ });
51
+ }
52
+
28
53
  const baseOptions = createServerEsbuildOptions({
29
54
  pkgDir: options.pkgDir,
30
55
  entryPoints: options.entryPoints,
@@ -35,6 +60,7 @@ export async function createContext(options: EsbuildContextOptions): Promise<voi
35
60
 
36
61
  context = await esbuild.context({
37
62
  ...baseOptions,
63
+ plugins: tscPlugin != null ? [tscPlugin.plugin] : [],
38
64
  metafile: true,
39
65
  write: false,
40
66
  });
@@ -51,7 +77,18 @@ export async function rebuild(): Promise<{
51
77
  } | null> {
52
78
  if (context == null) return null;
53
79
 
54
- const result = await context.rebuild();
80
+ let result: esbuild.BuildResult;
81
+ try {
82
+ result = await context.rebuild();
83
+ } catch (err) {
84
+ const tscErrors = tscPlugin?.getErrors() ?? [];
85
+ const allErrors = [errNs.message(err), ...tscErrors];
86
+ return {
87
+ success: false,
88
+ errors: allErrors.length > 0 ? allErrors : undefined,
89
+ warnings: undefined,
90
+ };
91
+ }
55
92
 
56
93
  if (result.metafile != null) {
57
94
  lastMetafile = result.metafile;
@@ -61,12 +98,14 @@ export async function rebuild(): Promise<{
61
98
  await writeChangedOutputFiles(result.outputFiles);
62
99
  }
63
100
 
64
- const errors = result.errors.map((e) => e.text);
101
+ const esbuildErrors = result.errors.map((e) => e.text);
102
+ const tscErrors = tscPlugin?.getErrors() ?? [];
103
+ const allErrors = [...esbuildErrors, ...tscErrors];
65
104
  const warnings = result.warnings.map((w) => w.text);
66
105
 
67
106
  return {
68
- success: result.errors.length === 0,
69
- errors: errors.length > 0 ? errors : undefined,
107
+ success: allErrors.length === 0,
108
+ errors: allErrors.length > 0 ? allErrors : undefined,
70
109
  warnings: warnings.length > 0 ? warnings : undefined,
71
110
  };
72
111
  }
@@ -85,6 +124,10 @@ export async function recreateContext(options: EsbuildContextOptions): Promise<v
85
124
  context = undefined;
86
125
  lastMetafile = undefined;
87
126
 
127
+ if (tscPlugin != null) {
128
+ tscPlugin.resetBuilderProgram();
129
+ }
130
+
88
131
  try {
89
132
  await createContext(options);
90
133
  } finally {
@@ -101,6 +144,7 @@ export async function dispose(): Promise<void> {
101
144
  const contextToDispose = context;
102
145
  context = undefined;
103
146
  lastMetafile = undefined;
147
+ tscPlugin = undefined;
104
148
 
105
149
  if (contextToDispose != null) {
106
150
  await contextToDispose.dispose();
@@ -120,3 +164,27 @@ export function getMetafile(): esbuild.Metafile | undefined {
120
164
  export function hasContext(): boolean {
121
165
  return context != null;
122
166
  }
167
+
168
+ /**
169
+ * tsc 플러그인의 ts.Program을 반환한다.
170
+ * 플러그인이 없으면 undefined를 반환한다.
171
+ */
172
+ export function getTscProgram(): ts.Program | undefined {
173
+ return tscPlugin?.getProgram();
174
+ }
175
+
176
+ /**
177
+ * tsc 플러그인의 affected files를 반환한다.
178
+ * 플러그인이 없으면 undefined를 반환한다.
179
+ */
180
+ export function getTscAffectedFiles(): ReadonlySet<string> | undefined {
181
+ return tscPlugin?.getAffectedFiles();
182
+ }
183
+
184
+ /**
185
+ * tsc 플러그인의 diagnostics를 반환한다.
186
+ * 플러그인이 없으면 빈 배열을 반환한다.
187
+ */
188
+ export function getTscDiagnostics(): SerializedDiagnostic[] {
189
+ return tscPlugin?.getDiagnostics() ?? [];
190
+ }
@@ -118,7 +118,7 @@ describe("createClientTransformStylesheet", () => {
118
118
  const deps = new Map<string, Set<string>>();
119
119
  const transform = createClientTransformStylesheet({
120
120
  loadPaths: [],
121
- postCssPlugins: [testPlugin],
121
+ postcssPlugins: [testPlugin],
122
122
  scssErrors: errors,
123
123
  scssDependencies: deps,
124
124
  });