@simplysm/sd-cli 14.0.38 → 14.0.39

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 (50) 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/esbuild/esbuild-client-config.d.ts +0 -2
  6. package/dist/esbuild/esbuild-client-config.d.ts.map +1 -1
  7. package/dist/esbuild/esbuild-client-config.js +19 -9
  8. package/dist/esbuild/esbuild-client-config.js.map +1 -1
  9. package/dist/esbuild/esbuild-postcss-plugin.d.ts +8 -0
  10. package/dist/esbuild/esbuild-postcss-plugin.d.ts.map +1 -0
  11. package/dist/esbuild/esbuild-postcss-plugin.js +105 -0
  12. package/dist/esbuild/esbuild-postcss-plugin.js.map +1 -0
  13. package/dist/esbuild/esbuild-tsc-plugin.d.ts +23 -0
  14. package/dist/esbuild/esbuild-tsc-plugin.d.ts.map +1 -0
  15. package/dist/esbuild/esbuild-tsc-plugin.js +60 -0
  16. package/dist/esbuild/esbuild-tsc-plugin.js.map +1 -0
  17. package/dist/workers/client.worker.d.ts.map +1 -1
  18. package/dist/workers/client.worker.js +32 -2
  19. package/dist/workers/client.worker.js.map +1 -1
  20. package/dist/workers/server-build.worker.d.ts.map +1 -1
  21. package/dist/workers/server-build.worker.js +129 -90
  22. package/dist/workers/server-build.worker.js.map +1 -1
  23. package/dist/workers/server-esbuild-context.d.ts +27 -0
  24. package/dist/workers/server-esbuild-context.d.ts.map +1 -1
  25. package/dist/workers/server-esbuild-context.js +43 -3
  26. package/dist/workers/server-esbuild-context.js.map +1 -1
  27. package/package.json +6 -4
  28. package/src/angular/angular-build-pipeline.ts +2 -2
  29. package/src/angular/client-transform-stylesheet.ts +4 -4
  30. package/src/esbuild/esbuild-client-config.ts +21 -12
  31. package/src/esbuild/esbuild-postcss-plugin.ts +117 -0
  32. package/src/esbuild/esbuild-tsc-plugin.ts +83 -0
  33. package/src/workers/client.worker.ts +32 -2
  34. package/src/workers/server-build.worker.ts +136 -97
  35. package/src/workers/server-esbuild-context.ts +59 -3
  36. package/tests/angular/client-transform-stylesheet.spec.ts +1 -1
  37. package/tests/esbuild/esbuild-tsc-plugin.acc.spec.ts +349 -0
  38. package/tests/esbuild/esbuild-tsc-plugin.spec.ts +230 -0
  39. package/tests/utils/esbuild-client-config-postcss.verify.md +6 -0
  40. package/tests/utils/esbuild-client-config.acc.spec.ts +26 -14
  41. package/tests/utils/esbuild-client-config.spec.ts +73 -11
  42. package/tests/utils/esbuild-postcss-plugin.acc.spec.ts +299 -0
  43. package/tests/utils/esbuild-postcss-plugin.spec.ts +290 -0
  44. package/tests/utils/esbuild-scss-plugin.acc.spec.ts +1 -0
  45. package/tests/workers/server-build-lint.spec.ts +43 -0
  46. package/tests/workers/server-build-worker-refactoring.verify.md +14 -0
  47. package/tests/workers/server-build-worker.spec.ts +122 -9
  48. package/tests/workers/server-esbuild-context-tsc.verify.md +7 -0
  49. package/tests/workers/server-esbuild-context.acc.spec.ts +156 -2
  50. package/tests/workers/server-esbuild-context.spec.ts +320 -2
@@ -0,0 +1,117 @@
1
+ import type esbuild from "esbuild";
2
+ import type { AcceptedPlugin } from "postcss";
3
+ import postcss from "postcss";
4
+ import fs from "fs";
5
+ import * as acorn from "acorn";
6
+ import * as walk from "acorn-walk";
7
+
8
+ export interface CreatePostcssPluginOptions {
9
+ /** 이미 로딩된 PostCSS 플러그인 인스턴스 배열 */
10
+ plugins: AcceptedPlugin[];
11
+ }
12
+
13
+ export function createPostcssPlugin(options: CreatePostcssPluginOptions): esbuild.Plugin {
14
+ return {
15
+ name: "sd-postcss",
16
+ setup(build) {
17
+ build.onEnd(async (result) => {
18
+ if (options.plugins.length === 0) return;
19
+ if (result.metafile == null) return;
20
+
21
+ const processor = postcss(options.plugins);
22
+ const outputFiles = Object.keys(result.metafile.outputs);
23
+
24
+ // .css 파일 처리
25
+ for (const file of outputFiles) {
26
+ if (!file.endsWith(".css")) continue;
27
+
28
+ try {
29
+ const css = await fs.promises.readFile(file, "utf-8");
30
+ const processed = await processor.process(css, { from: file });
31
+ await fs.promises.writeFile(file, processed.css);
32
+ } catch (e: unknown) {
33
+ result.errors.push({
34
+ id: "",
35
+ pluginName: "sd-postcss",
36
+ text: `PostCSS error in ${file}: ${e instanceof Error ? e.message : String(e)}`,
37
+ location: null,
38
+ notes: [],
39
+ detail: undefined,
40
+ });
41
+ }
42
+ }
43
+
44
+ // .js 파일 처리 — ɵɵdefineComponent 내 styles 배열의 문자열에 PostCSS 적용
45
+ for (const file of outputFiles) {
46
+ if (!file.endsWith(".js")) continue;
47
+
48
+ try {
49
+ const code = await fs.promises.readFile(file, "utf-8");
50
+ if (!code.includes("styles")) continue;
51
+
52
+ const ast = acorn.parse(code, {
53
+ ecmaVersion: "latest",
54
+ sourceType: "module",
55
+ });
56
+
57
+ // styles 배열 내 문자열 리터럴의 위치와 PostCSS 결과를 수집
58
+ const replacements: Array<{ start: number; end: number; text: string }> = [];
59
+
60
+ walk.ancestor(ast, {
61
+ Property(node: any, _state: any, ancestors: any[]) {
62
+ // key가 "styles"이고 value가 ArrayExpression인 Property만 대상
63
+ if (node.key.type !== "Identifier" || node.key.name !== "styles") return;
64
+ if (node.value.type !== "ArrayExpression") return;
65
+
66
+ // ancestors에서 ɵɵdefineComponent 호출을 확인
67
+ const inDefineComponent = ancestors.some(
68
+ (a: any) =>
69
+ a.type === "CallExpression" &&
70
+ a.callee?.type === "MemberExpression" &&
71
+ a.callee.property?.type === "Identifier" &&
72
+ a.callee.property.name === "\u0275\u0275defineComponent",
73
+ );
74
+ if (!inDefineComponent) return;
75
+
76
+ for (const element of node.value.elements) {
77
+ if (element == null) continue;
78
+ if (element.type !== "Literal" || typeof element.value !== "string") continue;
79
+
80
+ replacements.push({
81
+ start: element.start,
82
+ end: element.end,
83
+ text: element.value,
84
+ });
85
+ }
86
+ },
87
+ });
88
+
89
+ if (replacements.length === 0) continue;
90
+
91
+ // PostCSS 적용 및 역순 교체
92
+ let modified = code;
93
+ // 끝에서 시작 방향으로 교체하여 offset 유지
94
+ const sorted = replacements.sort((a, b) => b.start - a.start);
95
+
96
+ for (const rep of sorted) {
97
+ const processed = await processor.process(rep.text, { from: file });
98
+ const escaped = JSON.stringify(processed.css);
99
+ modified = modified.slice(0, rep.start) + escaped + modified.slice(rep.end);
100
+ }
101
+
102
+ await fs.promises.writeFile(file, modified);
103
+ } catch (e: unknown) {
104
+ result.errors.push({
105
+ id: "",
106
+ pluginName: "sd-postcss",
107
+ text: `PostCSS error in ${file}: ${e instanceof Error ? e.message : String(e)}`,
108
+ location: null,
109
+ notes: [],
110
+ detail: undefined,
111
+ });
112
+ }
113
+ }
114
+ });
115
+ },
116
+ };
117
+ }
@@ -0,0 +1,83 @@
1
+ import type esbuild from "esbuild";
2
+ import type ts from "typescript";
3
+ import { err as errNs } from "@simplysm/core-common";
4
+ import { parseTsconfig, type TypecheckEnv } from "../utils/tsconfig";
5
+ import { runTscPackageBuild, type TscPackageBuildResult } from "../utils/tsc-build";
6
+ import type { SerializedDiagnostic } from "../typecheck/typecheck-serialization";
7
+
8
+ export interface TscPluginOptions {
9
+ pkgDir: string;
10
+ cwd: string;
11
+ output: { dts: boolean };
12
+ env?: TypecheckEnv;
13
+ includeTests?: boolean;
14
+ }
15
+
16
+ export interface TscPluginResult {
17
+ plugin: esbuild.Plugin;
18
+ getProgram(): ts.Program | undefined;
19
+ getAffectedFiles(): ReadonlySet<string> | undefined;
20
+ getDiagnostics(): SerializedDiagnostic[];
21
+ getErrors(): string[] | undefined;
22
+ resetBuilderProgram(): void;
23
+ }
24
+
25
+ export function createTscPlugin(options: TscPluginOptions): TscPluginResult {
26
+ // 내부 상태
27
+ let lastProgram: ts.Program | undefined;
28
+ let lastAffectedFiles: ReadonlySet<string> | undefined;
29
+ let lastDiagnostics: SerializedDiagnostic[] = [];
30
+ let lastErrors: string[] | undefined;
31
+ let lastBuilderProgram: ts.EmitAndSemanticDiagnosticsBuilderProgram | undefined;
32
+
33
+ // onStart에서 생성한 tsc Promise (onEnd에서 await)
34
+ let tscPromise: Promise<TscPackageBuildResult> | undefined;
35
+
36
+ const plugin: esbuild.Plugin = {
37
+ name: "sd-tsc",
38
+ setup(build) {
39
+ build.onStart(() => {
40
+ // microtask로 tsc 스케줄링 (await하지 않음)
41
+ tscPromise = Promise.resolve().then(() => {
42
+ const parsedConfig = parseTsconfig(options.pkgDir);
43
+ return runTscPackageBuild({
44
+ pkgDir: options.pkgDir,
45
+ cwd: options.cwd,
46
+ output: { js: false, dts: options.output.dts },
47
+ parsedConfig,
48
+ env: options.env,
49
+ includeTests: options.includeTests,
50
+ oldBuilderProgram: lastBuilderProgram,
51
+ });
52
+ });
53
+ });
54
+
55
+ build.onEnd(async () => {
56
+ try {
57
+ const tscResult = await tscPromise!;
58
+ lastProgram = tscResult.program;
59
+ lastAffectedFiles = tscResult.affectedFiles;
60
+ lastDiagnostics = tscResult.diagnostics;
61
+ lastErrors = tscResult.errors;
62
+ lastBuilderProgram = tscResult.builderProgram;
63
+ } catch (err) {
64
+ lastProgram = undefined;
65
+ lastAffectedFiles = undefined;
66
+ lastDiagnostics = [];
67
+ lastErrors = [errNs.message(err)];
68
+ }
69
+ });
70
+ },
71
+ };
72
+
73
+ return {
74
+ plugin,
75
+ getProgram: () => lastProgram,
76
+ getAffectedFiles: () => lastAffectedFiles,
77
+ getDiagnostics: () => lastDiagnostics,
78
+ getErrors: () => lastErrors,
79
+ resetBuilderProgram: () => {
80
+ lastBuilderProgram = undefined;
81
+ },
82
+ };
83
+ }
@@ -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
 
@@ -226,17 +225,48 @@ async function startWatch(info: ClientBuildInfo): Promise<ClientBuildResult> {
226
225
  polyfills,
227
226
  legacyModule,
228
227
  postcssPlugins,
229
- postcssConfigPath: info.pkgDir,
230
228
  templateUpdates: legacyModule ? undefined : templateUpdates,
231
229
  plugins: [
232
230
  {
233
231
  name: "sd-build-start",
234
232
  setup(pluginBuild: esbuild.PluginBuild) {
233
+ const prevMtimes = new Map<string, number>();
234
+
235
235
  pluginBuild.onStart(() => {
236
+ // loadResultCache 무효화: 변경된 JS 파일의 캐시 엔트리 제거
237
+ if (esbuildResult != null) {
238
+ const { loadResultCache } = esbuildResult.sourceFileCache;
239
+ for (const file of loadResultCache.watchFiles) {
240
+ try {
241
+ const mtime = fs.statSync(file).mtimeMs;
242
+ const prev = prevMtimes.get(file);
243
+ if (prev != null && prev !== mtime) {
244
+ loadResultCache.invalidate(file);
245
+ }
246
+ } catch {
247
+ if (prevMtimes.has(file)) {
248
+ loadResultCache.invalidate(file);
249
+ }
250
+ }
251
+ }
252
+ }
253
+
236
254
  if (!isInitialBuild) {
237
255
  sender.send("buildStart", {});
238
256
  }
239
257
  });
258
+
259
+ pluginBuild.onEnd(() => {
260
+ if (esbuildResult == null) return;
261
+ prevMtimes.clear();
262
+ for (const file of esbuildResult.sourceFileCache.loadResultCache.watchFiles) {
263
+ try {
264
+ prevMtimes.set(file, fs.statSync(file).mtimeMs);
265
+ } catch {
266
+ // 삭제된 파일
267
+ }
268
+ }
269
+ });
240
270
  },
241
271
  },
242
272
  ],
@@ -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