@simplysm/sd-cli 14.0.39 → 14.0.41
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/dist/angular/angular-build-pipeline.d.ts +1 -0
- package/dist/angular/angular-build-pipeline.d.ts.map +1 -1
- package/dist/angular/angular-build-pipeline.js +4 -0
- package/dist/angular/angular-build-pipeline.js.map +1 -1
- package/dist/angular/angular-compiler.d.ts +1 -0
- package/dist/angular/angular-compiler.d.ts.map +1 -1
- package/dist/angular/angular-compiler.js +3 -0
- package/dist/angular/angular-compiler.js.map +1 -1
- package/dist/dev-server/hmr-client-script.d.ts +2 -2
- package/dist/dev-server/hmr-client-script.d.ts.map +1 -1
- package/dist/dev-server/hmr-client-script.js +4 -4
- package/dist/dev-server/hmr-client-script.js.map +1 -1
- package/dist/esbuild/esbuild-client-config.js +1 -1
- package/dist/esbuild/esbuild-client-config.js.map +1 -1
- package/dist/lint/lint-core.d.ts +0 -1
- package/dist/lint/lint-core.d.ts.map +1 -1
- package/dist/lint/lint-core.js +0 -3
- package/dist/lint/lint-core.js.map +1 -1
- package/dist/lint/lint-with-program.d.ts +0 -1
- package/dist/lint/lint-with-program.d.ts.map +1 -1
- package/dist/lint/lint-with-program.js +3 -13
- package/dist/lint/lint-with-program.js.map +1 -1
- package/dist/workers/build-watch-paths.js +2 -2
- package/dist/workers/build-watch-paths.js.map +1 -1
- package/dist/workers/client.worker.d.ts.map +1 -1
- package/dist/workers/client.worker.js +65 -27
- package/dist/workers/client.worker.js.map +1 -1
- package/dist/workers/ngtsc-build.worker.d.ts.map +1 -1
- package/dist/workers/ngtsc-build.worker.js +8 -0
- package/dist/workers/ngtsc-build.worker.js.map +1 -1
- package/dist/workers/server-esbuild-context.d.ts.map +1 -1
- package/dist/workers/server-esbuild-context.js +14 -1
- package/dist/workers/server-esbuild-context.js.map +1 -1
- package/package.json +4 -4
- package/src/angular/angular-build-pipeline.ts +5 -0
- package/src/angular/angular-compiler.ts +4 -0
- package/src/dev-server/hmr-client-script.ts +4 -4
- package/src/esbuild/esbuild-client-config.ts +1 -1
- package/src/lint/lint-core.ts +0 -3
- package/src/lint/lint-with-program.ts +3 -14
- package/src/workers/build-watch-paths.ts +2 -2
- package/src/workers/client.worker.ts +67 -30
- package/src/workers/ngtsc-build.worker.ts +9 -0
- package/src/workers/server-esbuild-context.ts +13 -1
- package/tests/angular/angular-build-pipeline.spec.ts +66 -0
- package/tests/utils/angular-compiler.spec.ts +35 -0
- package/tests/utils/esbuild-client-config.acc.spec.ts +8 -6
- package/tests/utils/esbuild-client-config.spec.ts +6 -5
- package/tests/utils/hmr-client-script.acc.spec.ts +8 -8
- package/tests/utils/hmr-client-script.spec.ts +5 -5
- package/tests/utils/lint-core.spec.ts +2 -3
- package/tests/utils/lint-with-program.spec.ts +6 -29
- package/tests/workers/build-watch-paths.acc.spec.ts +2 -2
- package/tests/workers/build-watch-paths.spec.ts +23 -2
- package/tests/workers/ngtsc-build-rootnames-refresh.verify.md +8 -0
- package/tests/workers/server-esbuild-context.acc.spec.ts +32 -0
- package/tests/workers/server-esbuild-context.spec.ts +81 -0
|
@@ -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();
|
|
@@ -210,8 +210,29 @@ async function startWatch(info: ClientBuildInfo): Promise<ClientBuildResult> {
|
|
|
210
210
|
const polyfills = fs.existsSync(polyfillsPath) ? ["src/polyfills.ts"] : undefined;
|
|
211
211
|
const entryNames = ["main", ...(polyfills != null ? ["polyfills"] : [])];
|
|
212
212
|
|
|
213
|
-
// 4. templateUpdates Map
|
|
213
|
+
// 4. templateUpdates Map
|
|
214
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;
|
|
215
236
|
let initialBuildResolve: ((result: ClientBuildResult) => void) | undefined;
|
|
216
237
|
let isInitialBuild = true;
|
|
217
238
|
|
|
@@ -233,22 +254,26 @@ async function startWatch(info: ClientBuildInfo): Promise<ClientBuildResult> {
|
|
|
233
254
|
const prevMtimes = new Map<string, number>();
|
|
234
255
|
|
|
235
256
|
pluginBuild.onStart(() => {
|
|
236
|
-
//
|
|
257
|
+
// sourceFileCache 무효화: 변경된 파일의 loadResultCache + TypeScript 소스 캐시 모두 제거
|
|
237
258
|
if (esbuildResult != null) {
|
|
238
259
|
const { loadResultCache } = esbuildResult.sourceFileCache;
|
|
260
|
+
const changedFiles = new Set<string>();
|
|
239
261
|
for (const file of loadResultCache.watchFiles) {
|
|
240
262
|
try {
|
|
241
263
|
const mtime = fs.statSync(file).mtimeMs;
|
|
242
264
|
const prev = prevMtimes.get(file);
|
|
243
265
|
if (prev != null && prev !== mtime) {
|
|
244
|
-
|
|
266
|
+
changedFiles.add(file);
|
|
245
267
|
}
|
|
246
268
|
} catch {
|
|
247
269
|
if (prevMtimes.has(file)) {
|
|
248
|
-
|
|
270
|
+
changedFiles.add(file);
|
|
249
271
|
}
|
|
250
272
|
}
|
|
251
273
|
}
|
|
274
|
+
if (changedFiles.size > 0) {
|
|
275
|
+
esbuildResult.sourceFileCache.invalidate(changedFiles);
|
|
276
|
+
}
|
|
252
277
|
}
|
|
253
278
|
|
|
254
279
|
if (!isInitialBuild) {
|
|
@@ -272,9 +297,10 @@ async function startWatch(info: ClientBuildInfo): Promise<ClientBuildResult> {
|
|
|
272
297
|
],
|
|
273
298
|
onEnd: async (result: esbuild.BuildResult) => {
|
|
274
299
|
try {
|
|
275
|
-
// index.html 재생성
|
|
300
|
+
// index.html 재생성 (lastMetafile 보관 — index.html 단독 변경 시 재생성용)
|
|
276
301
|
if (result.metafile != null) {
|
|
277
|
-
|
|
302
|
+
lastMetafile = result.metafile;
|
|
303
|
+
const hmrPostTransform = createHmrPostTransform(basePath, actualPort);
|
|
278
304
|
const indexPath = path.join(info.pkgDir, "src", "index.html");
|
|
279
305
|
const indexResult = await generateIndexHtml({
|
|
280
306
|
indexPath,
|
|
@@ -331,37 +357,42 @@ async function startWatch(info: ClientBuildInfo): Promise<ClientBuildResult> {
|
|
|
331
357
|
},
|
|
332
358
|
});
|
|
333
359
|
|
|
334
|
-
//
|
|
335
|
-
const httpDevServer = createDevHttpServer({
|
|
336
|
-
distDir: outdir,
|
|
337
|
-
basePath,
|
|
338
|
-
port: info.port ?? 0,
|
|
339
|
-
onRequest: (req, res) => hmrService?.handleRequest(req, res) ?? false,
|
|
340
|
-
});
|
|
341
|
-
devServer = httpDevServer;
|
|
342
|
-
|
|
343
|
-
// 6. HMR WebSocket 서비스 생성
|
|
344
|
-
hmrService = createHmrService({
|
|
345
|
-
httpServer: httpDevServer.httpServer,
|
|
346
|
-
basePath,
|
|
347
|
-
templateUpdates,
|
|
348
|
-
outDir: outdir,
|
|
349
|
-
});
|
|
350
|
-
|
|
351
|
-
// 7. esbuild watch 시작 + 초기 빌드 대기
|
|
360
|
+
// 8. esbuild watch 시작 + 초기 빌드 대기
|
|
352
361
|
await esbuildResult.context.watch();
|
|
353
362
|
|
|
354
363
|
const initialResult = await new Promise<ClientBuildResult>((resolve) => {
|
|
355
364
|
initialBuildResolve = resolve;
|
|
356
365
|
});
|
|
357
366
|
|
|
358
|
-
//
|
|
359
|
-
const
|
|
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
|
+
});
|
|
360
391
|
|
|
361
|
-
//
|
|
392
|
+
// 10. serverReady 이벤트 전송
|
|
362
393
|
sender.send("serverReady", { port: actualPort });
|
|
363
394
|
|
|
364
|
-
//
|
|
395
|
+
// 11. .config.json + .dev-port 기록
|
|
365
396
|
writeConfigJson(outdir, info.configs);
|
|
366
397
|
fs.writeFileSync(path.join(outdir, ".dev-port"), String(actualPort));
|
|
367
398
|
|
|
@@ -403,6 +434,12 @@ async function stopWatch(): Promise<void> {
|
|
|
403
434
|
publicWatcher = undefined;
|
|
404
435
|
}
|
|
405
436
|
|
|
437
|
+
// 5. index.html 감시 종료
|
|
438
|
+
if (indexHtmlWatcher != null) {
|
|
439
|
+
await indexHtmlWatcher.close();
|
|
440
|
+
indexHtmlWatcher = undefined;
|
|
441
|
+
}
|
|
442
|
+
|
|
406
443
|
logger.debug("esbuild watch 정리 완료");
|
|
407
444
|
}
|
|
408
445
|
|
|
@@ -296,6 +296,15 @@ async function startWatch(info: NgtscBuildInfo): Promise<void> {
|
|
|
296
296
|
|
|
297
297
|
sender.send("buildStart", {});
|
|
298
298
|
|
|
299
|
+
// 파일 추가/삭제 시 rootNames 재스캔
|
|
300
|
+
if (addOrRemove) {
|
|
301
|
+
const newParsedConfig = parseTsconfig(watchInfo!.pkgDir);
|
|
302
|
+
const newSourceFiles = watchInfo!.output.includeTests === true
|
|
303
|
+
? getPackageFiles(watchInfo!.pkgDir, newParsedConfig)
|
|
304
|
+
: getPackageSourceFiles(watchInfo!.pkgDir, newParsedConfig);
|
|
305
|
+
pipeline.updateRootNames(newSourceFiles);
|
|
306
|
+
}
|
|
307
|
+
|
|
299
308
|
// Pipeline 증분 업데이트 (SCSS 의존성 초기화 포함)
|
|
300
309
|
pipeline.clearScssDependencies();
|
|
301
310
|
const updateResult = await pipeline.update(modifiedFiles);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type ts from "typescript";
|
|
2
2
|
import esbuild from "esbuild";
|
|
3
|
+
import { err as errNs } from "@simplysm/core-common";
|
|
3
4
|
import {
|
|
4
5
|
createServerEsbuildOptions,
|
|
5
6
|
writeChangedOutputFiles,
|
|
@@ -76,7 +77,18 @@ export async function rebuild(): Promise<{
|
|
|
76
77
|
} | null> {
|
|
77
78
|
if (context == null) return null;
|
|
78
79
|
|
|
79
|
-
|
|
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
|
+
}
|
|
80
92
|
|
|
81
93
|
if (result.metafile != null) {
|
|
82
94
|
lastMetafile = result.metafile;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
2
|
import path from "path";
|
|
3
|
+
import fs from "node:fs";
|
|
3
4
|
import ts from "typescript";
|
|
4
5
|
import { AngularBuildPipeline } from "../../src/angular/angular-build-pipeline.js";
|
|
5
6
|
import { AngularSourceFileCache } from "../../src/angular/angular-compiler.js";
|
|
@@ -168,6 +169,71 @@ describe("AngularBuildPipeline", () => {
|
|
|
168
169
|
expect(pipeline.getEmittedFile(appPath)).toBeDefined();
|
|
169
170
|
});
|
|
170
171
|
|
|
172
|
+
// --- updateRootNames ---
|
|
173
|
+
|
|
174
|
+
// Acceptance: Scenario "updateRootNames 호출 시 compiler까지 전파"
|
|
175
|
+
it("updateRootNames로 새 파일 추가 후 update가 성공하고 새 파일이 emit된다", async () => {
|
|
176
|
+
const pipeline = new AngularBuildPipeline(
|
|
177
|
+
createPipelineOptions("library", { sourceFileCache: new AngularSourceFileCache() }),
|
|
178
|
+
);
|
|
179
|
+
await pipeline.initialize();
|
|
180
|
+
|
|
181
|
+
const tempPath = path.join(FIXTURE_DIR, "src/temp-update-root-names-test.ts");
|
|
182
|
+
fs.writeFileSync(tempPath, "export const tempRootNamesValue = 42;", "utf-8");
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
const parsed = parseFixtureConfig();
|
|
186
|
+
const newRootNames = getPackageSourceFiles(FIXTURE_DIR, parsed);
|
|
187
|
+
pipeline.updateRootNames(newRootNames);
|
|
188
|
+
|
|
189
|
+
const result = await pipeline.update([tempPath]);
|
|
190
|
+
expect(result.diagnostics.errors).toHaveLength(0);
|
|
191
|
+
expect(pipeline.getEmittedFile(tempPath.replace(/\\/g, "/"))).toBeDefined();
|
|
192
|
+
} finally {
|
|
193
|
+
fs.unlinkSync(tempPath);
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// Acceptance: Scenario "기존 소스 파일 삭제 시 rootNames에서 제거"
|
|
198
|
+
it("updateRootNames로 파일 제거 후 update에서 해당 파일이 프로그램에서 제외된다", async () => {
|
|
199
|
+
const tempPath = path.join(FIXTURE_DIR, "src/temp-to-remove.ts");
|
|
200
|
+
fs.writeFileSync(tempPath, "export const toRemove = 1;", "utf-8");
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
const sourceFileCache = new AngularSourceFileCache();
|
|
204
|
+
const pipeline = new AngularBuildPipeline(
|
|
205
|
+
createPipelineOptions("library", { sourceFileCache }),
|
|
206
|
+
);
|
|
207
|
+
await pipeline.initialize();
|
|
208
|
+
|
|
209
|
+
// 파일이 초기 프로그램에 포함되어 있는지 확인
|
|
210
|
+
const initialFiles = pipeline.getTsProgram().getSourceFiles().map((sf) => sf.fileName.replace(/\\/g, "/"));
|
|
211
|
+
expect(initialFiles.some((f) => f.includes("temp-to-remove.ts"))).toBe(true);
|
|
212
|
+
|
|
213
|
+
// 파일 삭제 시뮬레이션: 디스크에서 삭제 후 rootNames 재스캔
|
|
214
|
+
fs.unlinkSync(tempPath);
|
|
215
|
+
const parsed = parseFixtureConfig();
|
|
216
|
+
const newRootNames = getPackageSourceFiles(FIXTURE_DIR, parsed);
|
|
217
|
+
pipeline.updateRootNames(newRootNames);
|
|
218
|
+
|
|
219
|
+
const result = await pipeline.update([tempPath]);
|
|
220
|
+
expect(result.diagnostics.errors).toHaveLength(0);
|
|
221
|
+
|
|
222
|
+
// 삭제된 파일이 프로그램에서 제외되었는지 확인
|
|
223
|
+
const updatedFiles = pipeline.getTsProgram().getSourceFiles().map((sf) => sf.fileName.replace(/\\/g, "/"));
|
|
224
|
+
expect(updatedFiles.some((f) => f.includes("temp-to-remove.ts"))).toBe(false);
|
|
225
|
+
} finally {
|
|
226
|
+
if (fs.existsSync(tempPath)) {
|
|
227
|
+
fs.unlinkSync(tempPath);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("updateRootNames가 compiler 미초기화 상태에서도 에러 없이 동작한다", () => {
|
|
233
|
+
const pipeline = new AngularBuildPipeline(createPipelineOptions("library"));
|
|
234
|
+
expect(() => pipeline.updateRootNames(["src/new.ts"])).not.toThrow();
|
|
235
|
+
});
|
|
236
|
+
|
|
171
237
|
// --- getPackageSourceFiles ---
|
|
172
238
|
|
|
173
239
|
it("getPackageSourceFiles includes fixture files by default", () => {
|
|
@@ -283,6 +283,41 @@ describe("AngularCompiler — Unit Tests", () => {
|
|
|
283
283
|
});
|
|
284
284
|
});
|
|
285
285
|
|
|
286
|
+
// =============================================================================
|
|
287
|
+
// AngularCompiler — updateRootNames
|
|
288
|
+
// =============================================================================
|
|
289
|
+
|
|
290
|
+
describe("AngularCompiler — updateRootNames", () => {
|
|
291
|
+
// Acceptance: Scenario "updateRootNames 호출 시 compiler까지 전파"
|
|
292
|
+
it("updateRootNames() 후 initialize()에서 새 rootNames가 NgtscProgram에 전달된다", async () => {
|
|
293
|
+
const compiler = new AngularCompiler({
|
|
294
|
+
rootNames: ["src/main.ts"],
|
|
295
|
+
compilerOptions: { target: ts.ScriptTarget.ESNext },
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
await compiler.initialize();
|
|
299
|
+
expect(ngtscConstructorSpy.mock.calls[0][0]).toEqual(["src/main.ts"]);
|
|
300
|
+
|
|
301
|
+
compiler.updateRootNames(["src/main.ts", "src/new.ts"]);
|
|
302
|
+
await compiler.initialize();
|
|
303
|
+
|
|
304
|
+
expect(ngtscConstructorSpy.mock.calls[1][0]).toEqual(["src/main.ts", "src/new.ts"]);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it("updateRootNames()로 빈 배열 설정 후 initialize()에서 빈 rootNames가 전달된다", async () => {
|
|
308
|
+
const compiler = new AngularCompiler({
|
|
309
|
+
rootNames: ["src/main.ts"],
|
|
310
|
+
compilerOptions: { target: ts.ScriptTarget.ESNext },
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
await compiler.initialize();
|
|
314
|
+
compiler.updateRootNames([]);
|
|
315
|
+
await compiler.initialize();
|
|
316
|
+
|
|
317
|
+
expect(ngtscConstructorSpy.mock.calls[1][0]).toEqual([]);
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
|
|
286
321
|
// =============================================================================
|
|
287
322
|
// AngularCompiler — 초기화
|
|
288
323
|
// =============================================================================
|
|
@@ -432,8 +432,8 @@ describe("createClientEsbuildContext — Acceptance", () => {
|
|
|
432
432
|
);
|
|
433
433
|
});
|
|
434
434
|
|
|
435
|
-
// Scenario:
|
|
436
|
-
it("plugins 배열이 [angularPlugin, scssPlugin,
|
|
435
|
+
// Scenario: customPlugins가 angularPlugin 이전에 배치된다 (onStart에서 sourceFileCache 무효화 선행 필요)
|
|
436
|
+
it("plugins 배열이 [customPlugins, angularPlugin, scssPlugin, onEndPlugin] 순서로 구성된다", async () => {
|
|
437
437
|
const customPlugin = { name: "custom", setup: vi.fn() };
|
|
438
438
|
await createClientEsbuildContext({
|
|
439
439
|
pkgDir: "/workspace/packages/my-app",
|
|
@@ -446,13 +446,15 @@ describe("createClientEsbuildContext — Acceptance", () => {
|
|
|
446
446
|
const esbuildOptions = vi.mocked(esbuild.context).mock.calls[0][0];
|
|
447
447
|
const pluginNames = esbuildOptions.plugins!.map((p: any) => p.name);
|
|
448
448
|
|
|
449
|
-
expect(pluginNames[0]).toBe("angular-compiler");
|
|
450
|
-
expect(pluginNames[1]).toBe("sd-scss");
|
|
451
449
|
expect(pluginNames).toContain("custom");
|
|
450
|
+
expect(pluginNames).toContain("angular-compiler");
|
|
451
|
+
expect(pluginNames).toContain("sd-scss");
|
|
452
452
|
expect(pluginNames[pluginNames.length - 1]).toBe("sd-on-end");
|
|
453
453
|
|
|
454
|
-
const scssIdx = pluginNames.indexOf("sd-scss");
|
|
455
454
|
const customIdx = pluginNames.indexOf("custom");
|
|
456
|
-
|
|
455
|
+
const angularIdx = pluginNames.indexOf("angular-compiler");
|
|
456
|
+
const scssIdx = pluginNames.indexOf("sd-scss");
|
|
457
|
+
expect(customIdx).toBeLessThan(angularIdx);
|
|
458
|
+
expect(angularIdx).toBeLessThan(scssIdx);
|
|
457
459
|
});
|
|
458
460
|
});
|
|
@@ -439,14 +439,17 @@ describe("createClientEsbuildContext — onEnd 플러그인", () => {
|
|
|
439
439
|
expect(pluginNames[pluginNames.length - 1]).toBe("sd-on-end");
|
|
440
440
|
});
|
|
441
441
|
|
|
442
|
-
it("angularPlugin
|
|
442
|
+
it("customPlugins가 angularPlugin 이전에 위치 (onStart에서 sourceFileCache 무효화 선행)", async () => {
|
|
443
443
|
await createClientEsbuildContext({
|
|
444
444
|
...baseDev,
|
|
445
445
|
plugins: [{ name: "custom", setup: vi.fn() } as any],
|
|
446
446
|
onEnd: vi.fn(),
|
|
447
447
|
});
|
|
448
448
|
const opts = vi.mocked(esbuild.context).mock.calls[0][0];
|
|
449
|
-
|
|
449
|
+
const pluginNames = opts.plugins!.map((p: any) => p.name);
|
|
450
|
+
const customIdx = pluginNames.indexOf("custom");
|
|
451
|
+
const angularIdx = pluginNames.indexOf("angular-compiler");
|
|
452
|
+
expect(customIdx).toBeLessThan(angularIdx);
|
|
450
453
|
});
|
|
451
454
|
});
|
|
452
455
|
|
|
@@ -481,7 +484,7 @@ describe("createClientEsbuildContext — SCSS 플러그인 통합", () => {
|
|
|
481
484
|
expect(pluginNames).toContain("sd-scss");
|
|
482
485
|
});
|
|
483
486
|
|
|
484
|
-
it("sd-scss 플러그인이 angularPlugin
|
|
487
|
+
it("sd-scss 플러그인이 angularPlugin 다음에 위치", async () => {
|
|
485
488
|
const customPlugin = { name: "custom", setup: vi.fn() };
|
|
486
489
|
await createClientEsbuildContext({
|
|
487
490
|
...baseDev,
|
|
@@ -492,10 +495,8 @@ describe("createClientEsbuildContext — SCSS 플러그인 통합", () => {
|
|
|
492
495
|
|
|
493
496
|
const angularIdx = pluginNames.indexOf("angular-compiler");
|
|
494
497
|
const scssIdx = pluginNames.indexOf("sd-scss");
|
|
495
|
-
const customIdx = pluginNames.indexOf("custom");
|
|
496
498
|
|
|
497
499
|
expect(scssIdx).toBe(angularIdx + 1);
|
|
498
|
-
expect(scssIdx).toBeLessThan(customIdx);
|
|
499
500
|
});
|
|
500
501
|
});
|
|
501
502
|
|
|
@@ -5,7 +5,7 @@ import { getHmrClientScript, createHmrPostTransform } from "../../src/dev-server
|
|
|
5
5
|
describe("HMR 클라이언트 스크립트 통합", () => {
|
|
6
6
|
describe("Scenario: HMR 클라이언트 문법 호환성", () => {
|
|
7
7
|
it("Chrome 61 비호환 문법을 사용하지 않는다", () => {
|
|
8
|
-
const script = getHmrClientScript("/app/");
|
|
8
|
+
const script = getHmrClientScript("/app/", 4200);
|
|
9
9
|
|
|
10
10
|
// optional chaining (?.) 미사용
|
|
11
11
|
expect(script).not.toMatch(/\?\./);
|
|
@@ -16,13 +16,13 @@ describe("HMR 클라이언트 스크립트 통합", () => {
|
|
|
16
16
|
});
|
|
17
17
|
|
|
18
18
|
it("WebSocket 연결 코드를 포함한다", () => {
|
|
19
|
-
const script = getHmrClientScript("/app/");
|
|
19
|
+
const script = getHmrClientScript("/app/", 4200);
|
|
20
20
|
expect(script).toContain("WebSocket");
|
|
21
21
|
expect(script).toContain("ws://");
|
|
22
22
|
});
|
|
23
23
|
|
|
24
24
|
it("component-update, css-update, full-reload 메시지 핸들러를 포함한다", () => {
|
|
25
|
-
const script = getHmrClientScript("/app/");
|
|
25
|
+
const script = getHmrClientScript("/app/", 4200);
|
|
26
26
|
expect(script).toContain("component-update");
|
|
27
27
|
expect(script).toContain("css-update");
|
|
28
28
|
expect(script).toContain("full-reload");
|
|
@@ -31,7 +31,7 @@ describe("HMR 클라이언트 스크립트 통합", () => {
|
|
|
31
31
|
});
|
|
32
32
|
|
|
33
33
|
it("자동 재연결 로직을 포함한다", () => {
|
|
34
|
-
const script = getHmrClientScript("/app/");
|
|
34
|
+
const script = getHmrClientScript("/app/", 4200);
|
|
35
35
|
expect(script).toContain("setTimeout");
|
|
36
36
|
expect(script).toContain("connect");
|
|
37
37
|
});
|
|
@@ -77,7 +77,7 @@ describe("HMR 클라이언트 스크립트 통합", () => {
|
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
it("css-update 시 msg.files와 매칭되는 link만 cache-busting 적용", () => {
|
|
80
|
-
const script = getHmrClientScript("/app/");
|
|
80
|
+
const script = getHmrClientScript("/app/", 4200);
|
|
81
81
|
const { sandbox, triggerMessage, mainLink, vendorLink } = createScriptEnv();
|
|
82
82
|
|
|
83
83
|
runInNewContext(script, sandbox);
|
|
@@ -89,7 +89,7 @@ describe("HMR 클라이언트 스크립트 통합", () => {
|
|
|
89
89
|
});
|
|
90
90
|
|
|
91
91
|
it("css-update 시 files에 여러 파일이 있으면 매칭되는 모든 link를 업데이트", () => {
|
|
92
|
-
const script = getHmrClientScript("/app/");
|
|
92
|
+
const script = getHmrClientScript("/app/", 4200);
|
|
93
93
|
const { sandbox, triggerMessage, mainLink, vendorLink } = createScriptEnv();
|
|
94
94
|
|
|
95
95
|
runInNewContext(script, sandbox);
|
|
@@ -107,7 +107,7 @@ describe("HMR 클라이언트 스크립트 통합", () => {
|
|
|
107
107
|
|
|
108
108
|
describe("Scenario: 스크립트 주입", () => {
|
|
109
109
|
it("postTransform이 </body> 직전에 script 태그를 삽입한다", async () => {
|
|
110
|
-
const transform = createHmrPostTransform("/app/");
|
|
110
|
+
const transform = createHmrPostTransform("/app/", 4200);
|
|
111
111
|
const html = "<html><body><div>content</div></body></html>";
|
|
112
112
|
const result = await transform(html);
|
|
113
113
|
|
|
@@ -117,7 +117,7 @@ describe("HMR 클라이언트 스크립트 통합", () => {
|
|
|
117
117
|
});
|
|
118
118
|
|
|
119
119
|
it("</body>가 없는 HTML에서도 스크립트를 추가한다", async () => {
|
|
120
|
-
const transform = createHmrPostTransform("/app/");
|
|
120
|
+
const transform = createHmrPostTransform("/app/", 4200);
|
|
121
121
|
const html = "<html><body><div>content</div>";
|
|
122
122
|
const result = await transform(html);
|
|
123
123
|
|
|
@@ -3,19 +3,19 @@ import { getHmrClientScript, createHmrPostTransform } from "../../src/dev-server
|
|
|
3
3
|
|
|
4
4
|
describe("getHmrClientScript", () => {
|
|
5
5
|
it("유효한 JavaScript를 생성한다", () => {
|
|
6
|
-
const script = getHmrClientScript("/app/");
|
|
6
|
+
const script = getHmrClientScript("/app/", 4200);
|
|
7
7
|
// new Function으로 구문 오류 확인
|
|
8
8
|
expect(() => new Function(script)).not.toThrow();
|
|
9
9
|
});
|
|
10
10
|
|
|
11
11
|
it("IIFE로 감싸져 있다", () => {
|
|
12
|
-
const script = getHmrClientScript("/app/");
|
|
12
|
+
const script = getHmrClientScript("/app/", 4200);
|
|
13
13
|
expect(script.trimStart()).toMatch(/^\(function\(\)/);
|
|
14
14
|
expect(script.trimEnd()).toMatch(/\}\)\(\);$/);
|
|
15
15
|
});
|
|
16
16
|
|
|
17
17
|
it("const/let 대신 var를 사용한다", () => {
|
|
18
|
-
const script = getHmrClientScript("/app/");
|
|
18
|
+
const script = getHmrClientScript("/app/", 4200);
|
|
19
19
|
// var 사용 확인
|
|
20
20
|
expect(script).toContain("var ws");
|
|
21
21
|
// const/let 미사용 확인
|
|
@@ -26,7 +26,7 @@ describe("getHmrClientScript", () => {
|
|
|
26
26
|
|
|
27
27
|
describe("createHmrPostTransform", () => {
|
|
28
28
|
it("여러 </body> 태그가 있으면 마지막 것 앞에 주입한다", async () => {
|
|
29
|
-
const transform = createHmrPostTransform("/app/");
|
|
29
|
+
const transform = createHmrPostTransform("/app/", 4200);
|
|
30
30
|
const html = "<body>first</body><body>second</body>";
|
|
31
31
|
const result = await transform(html);
|
|
32
32
|
|
|
@@ -37,7 +37,7 @@ describe("createHmrPostTransform", () => {
|
|
|
37
37
|
});
|
|
38
38
|
|
|
39
39
|
it("빈 HTML에도 스크립트를 추가한다", async () => {
|
|
40
|
-
const transform = createHmrPostTransform("/");
|
|
40
|
+
const transform = createHmrPostTransform("/", 4200);
|
|
41
41
|
const result = await transform("");
|
|
42
42
|
expect(result).toContain("<script>");
|
|
43
43
|
});
|
|
@@ -134,13 +134,12 @@ describe("executeLint", () => {
|
|
|
134
134
|
else process.env["TIMING"] = origTiming;
|
|
135
135
|
});
|
|
136
136
|
|
|
137
|
-
it("creates ESLint
|
|
137
|
+
it("creates ESLint without cache", async () => {
|
|
138
138
|
await executeLint({ targets: [], fix: false, timing: false });
|
|
139
139
|
|
|
140
140
|
expect(mocks.eslintCtor).toHaveBeenCalledWith(
|
|
141
|
-
expect.objectContaining({
|
|
141
|
+
expect.not.objectContaining({
|
|
142
142
|
cache: true,
|
|
143
|
-
cacheLocation: expect.stringContaining("eslint.cache"),
|
|
144
143
|
}),
|
|
145
144
|
);
|
|
146
145
|
});
|
|
@@ -288,8 +288,8 @@ describe("LintWithProgramRunner", () => {
|
|
|
288
288
|
});
|
|
289
289
|
});
|
|
290
290
|
|
|
291
|
-
describe("Scenario: ESLint cache
|
|
292
|
-
it("
|
|
291
|
+
describe("Scenario: ESLint cache is disabled", () => {
|
|
292
|
+
it("does not pass cache option to ESLint", async () => {
|
|
293
293
|
const program = createMockProgram([
|
|
294
294
|
createMockSourceFile("/workspace/packages/my-pkg/src/a.ts"),
|
|
295
295
|
]);
|
|
@@ -299,37 +299,14 @@ describe("LintWithProgramRunner", () => {
|
|
|
299
299
|
pkgName: "my-pkg",
|
|
300
300
|
});
|
|
301
301
|
|
|
302
|
-
// When: lint with affectedFiles (watch rebuild)
|
|
303
302
|
await runner.lint({
|
|
304
303
|
program: program as any,
|
|
305
|
-
affectedFiles: new Set(["/workspace/packages/my-pkg/src/a.ts"]),
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
// Then: ESLint is created with cache: false
|
|
309
|
-
expect(MockESLintClass).toHaveBeenCalledWith(
|
|
310
|
-
expect.objectContaining({ cache: false }),
|
|
311
|
-
);
|
|
312
|
-
});
|
|
313
|
-
|
|
314
|
-
it("enables ESLint cache when affectedFiles is not provided (one-time build)", async () => {
|
|
315
|
-
const program = createMockProgram([
|
|
316
|
-
createMockSourceFile("/workspace/packages/my-pkg/src/a.ts"),
|
|
317
|
-
]);
|
|
318
|
-
|
|
319
|
-
const runner = new LintWithProgramRunner({
|
|
320
|
-
cwd: "/workspace",
|
|
321
|
-
pkgName: "my-pkg",
|
|
322
304
|
});
|
|
323
305
|
|
|
324
|
-
//
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
// Then: ESLint is created with cache: true
|
|
330
|
-
expect(MockESLintClass).toHaveBeenCalledWith(
|
|
331
|
-
expect.objectContaining({ cache: true }),
|
|
332
|
-
);
|
|
306
|
+
// cache 옵션이 전달되지 않아야 한다
|
|
307
|
+
const ctorArg = MockESLintClass.mock.calls[0][0] as Record<string, unknown>;
|
|
308
|
+
expect(ctorArg).not.toHaveProperty("cache");
|
|
309
|
+
expect(ctorArg).not.toHaveProperty("cacheLocation");
|
|
333
310
|
});
|
|
334
311
|
});
|
|
335
312
|
|
|
@@ -37,9 +37,9 @@ describe("buildWatchPaths", () => {
|
|
|
37
37
|
expect(result.watchPaths).toContainEqual(
|
|
38
38
|
expect.stringContaining("core-common/src/**/*.ts"),
|
|
39
39
|
);
|
|
40
|
-
// replaceDeps dist (cwd + pkgDir 두
|
|
40
|
+
// replaceDeps dist (cwd + pkgDir 두 위치, js + dts)
|
|
41
41
|
expect(result.watchPaths).toContainEqual(
|
|
42
|
-
expect.stringContaining("node_modules/@external/lib/dist/**/*.{js,mjs,cjs}"),
|
|
42
|
+
expect.stringContaining("node_modules/@external/lib/dist/**/*.{js,mjs,cjs,d.ts,d.mts,d.cts}"),
|
|
43
43
|
);
|
|
44
44
|
// deps 결과 반환
|
|
45
45
|
expect(result.deps.workspaceDeps).toEqual(["core-common"]);
|
|
@@ -63,17 +63,38 @@ describe("buildWatchPaths", () => {
|
|
|
63
63
|
// cwd node_modules
|
|
64
64
|
expect(
|
|
65
65
|
watchPaths.some((p) =>
|
|
66
|
-
p.includes("/ws/node_modules/@scope/pkg/dist/**/*.{js,mjs,cjs}"),
|
|
66
|
+
p.includes("/ws/node_modules/@scope/pkg/dist/**/*.{js,mjs,cjs,d.ts,d.mts,d.cts}"),
|
|
67
67
|
),
|
|
68
68
|
).toBe(true);
|
|
69
69
|
// pkgDir node_modules
|
|
70
70
|
expect(
|
|
71
71
|
watchPaths.some((p) =>
|
|
72
|
-
p.includes("lib/node_modules/@scope/pkg/dist/**/*.{js,mjs,cjs}"),
|
|
72
|
+
p.includes("lib/node_modules/@scope/pkg/dist/**/*.{js,mjs,cjs,d.ts,d.mts,d.cts}"),
|
|
73
73
|
),
|
|
74
74
|
).toBe(true);
|
|
75
75
|
});
|
|
76
76
|
|
|
77
|
+
it("includes d.ts extensions in replaceDeps paths for non-scoped packages", () => {
|
|
78
|
+
mockCollectDeps.mockReturnValue({
|
|
79
|
+
workspaceDeps: [],
|
|
80
|
+
replaceDeps: ["some-lib"],
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const { watchPaths } = buildWatchPaths({
|
|
84
|
+
pkgDir: "/ws/packages/lib",
|
|
85
|
+
cwd: "/ws",
|
|
86
|
+
srcGlobs: ["*.ts"],
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const dtsGlob = "*.{js,mjs,cjs,d.ts,d.mts,d.cts}";
|
|
90
|
+
expect(
|
|
91
|
+
watchPaths.some((p) => p.includes(`/ws/node_modules/some-lib/dist/**/${dtsGlob}`)),
|
|
92
|
+
).toBe(true);
|
|
93
|
+
expect(
|
|
94
|
+
watchPaths.some((p) => p.includes(`lib/node_modules/some-lib/dist/**/${dtsGlob}`)),
|
|
95
|
+
).toBe(true);
|
|
96
|
+
});
|
|
97
|
+
|
|
77
98
|
it("includes extraDirs globs for each directory", () => {
|
|
78
99
|
mockCollectDeps.mockReturnValue({
|
|
79
100
|
workspaceDeps: ["dep-a"],
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# ngtsc watch rootNames 갱신 — LLM 검증
|
|
2
|
+
|
|
3
|
+
## 검증 항목
|
|
4
|
+
|
|
5
|
+
- [x] onChange에서 addOrRemove === true일 때 parseTsconfig + getPackageSourceFiles + pipeline.updateRootNames 호출: `ngtsc-build.worker.ts:299-306` — `if (addOrRemove)` 블록 내에서 `parseTsconfig(watchInfo!.pkgDir)` → `getPackageSourceFiles()` 또는 `getPackageFiles()` → `pipeline.updateRootNames(newSourceFiles)` 호출 체인이 정확히 구현됨
|
|
6
|
+
- [x] includeTests 분기가 초기 설정(line 233-235)과 동일 패턴: 초기 설정 `watchInfo.output.includeTests === true ? getPackageFiles(...) : getPackageSourceFiles(...)` (line 233-235)와 onChange 내 분기 `watchInfo!.output.includeTests === true ? getPackageFiles(...) : getPackageSourceFiles(...)` (line 302-304)가 동일한 패턴을 사용
|
|
7
|
+
- [x] addOrRemove === false일 때 rootNames 미갱신: `if (addOrRemove)` 블록(line 300-306) 내부에서만 rootNames 재스캔이 수행되므로, addOrRemove가 false일 때는 `pipeline.updateRootNames()`가 호출되지 않음. `shouldSkipRebuild`(line 293)에서 `hasAddOrRemove`가 false이고 변경 파일이 lastSourceFilePaths에 있으면 정상적으로 `pipeline.update(modifiedFiles)`만 실행됨
|
|
8
|
+
- [x] rootNames 재스캔이 shouldSkipRebuild 이후 + pipeline.update() 이전에 위치: line 293에서 shouldSkipRebuild 체크 → line 299에서 rootNames 재스캔 → line 310에서 pipeline.update(). 올바른 순서
|