@simplysm/sd-cli 14.0.43 → 14.0.45
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/ngtsc-build-core.d.ts +12 -3
- package/dist/angular/ngtsc-build-core.d.ts.map +1 -1
- package/dist/angular/ngtsc-build-core.js +70 -6
- package/dist/angular/ngtsc-build-core.js.map +1 -1
- package/dist/commands/publish/version-upgrade.d.ts.map +1 -1
- package/dist/commands/publish/version-upgrade.js +15 -12
- package/dist/commands/publish/version-upgrade.js.map +1 -1
- package/dist/deps/replace-deps/replace-deps-resolve.d.ts.map +1 -1
- package/dist/deps/replace-deps/replace-deps-resolve.js +6 -7
- package/dist/deps/replace-deps/replace-deps-resolve.js.map +1 -1
- package/dist/deps/replace-deps/replace-deps.d.ts.map +1 -1
- package/dist/deps/replace-deps/replace-deps.js +79 -15
- package/dist/deps/replace-deps/replace-deps.js.map +1 -1
- package/dist/deps/server-externals/server-production-files.d.ts +10 -5
- package/dist/deps/server-externals/server-production-files.d.ts.map +1 -1
- package/dist/deps/server-externals/server-production-files.js +22 -26
- package/dist/deps/server-externals/server-production-files.js.map +1 -1
- package/dist/esbuild/esbuild-angular-compiler-plugin.d.ts +3 -8
- package/dist/esbuild/esbuild-angular-compiler-plugin.d.ts.map +1 -1
- package/dist/esbuild/esbuild-angular-compiler-plugin.js +57 -83
- package/dist/esbuild/esbuild-angular-compiler-plugin.js.map +1 -1
- package/dist/esbuild/esbuild-postcss-plugin.d.ts.map +1 -1
- package/dist/esbuild/esbuild-postcss-plugin.js +9 -6
- package/dist/esbuild/esbuild-postcss-plugin.js.map +1 -1
- package/dist/esbuild/esbuild-worker-plugin.d.ts +30 -0
- package/dist/esbuild/esbuild-worker-plugin.d.ts.map +1 -0
- package/dist/esbuild/esbuild-worker-plugin.js +197 -0
- package/dist/esbuild/esbuild-worker-plugin.js.map +1 -0
- package/dist/workers/client.worker.d.ts.map +1 -1
- package/dist/workers/client.worker.js +4 -25
- package/dist/workers/client.worker.js.map +1 -1
- package/dist/workers/incremental-mtime-tracker.d.ts +13 -0
- package/dist/workers/incremental-mtime-tracker.d.ts.map +1 -0
- package/dist/workers/incremental-mtime-tracker.js +65 -0
- package/dist/workers/incremental-mtime-tracker.js.map +1 -0
- package/dist/workers/library-build.worker.d.ts.map +1 -1
- package/dist/workers/library-build.worker.js +37 -15
- package/dist/workers/library-build.worker.js.map +1 -1
- package/dist/workers/server-build.worker.d.ts.map +1 -1
- package/dist/workers/server-build.worker.js +6 -5
- package/dist/workers/server-build.worker.js.map +1 -1
- package/dist/workers/server-esbuild-context.d.ts.map +1 -1
- package/dist/workers/server-esbuild-context.js +3 -1
- package/dist/workers/server-esbuild-context.js.map +1 -1
- package/dist/workers/server-watch-manager.js +1 -1
- package/dist/workers/server-watch-manager.js.map +1 -1
- package/package.json +5 -4
- package/src/angular/ngtsc-build-core.ts +73 -5
- package/src/commands/publish/version-upgrade.ts +43 -34
- package/src/deps/replace-deps/replace-deps-resolve.ts +12 -7
- package/src/deps/replace-deps/replace-deps.ts +90 -16
- package/src/deps/server-externals/server-production-files.ts +26 -28
- package/src/esbuild/esbuild-angular-compiler-plugin.ts +82 -123
- package/src/esbuild/esbuild-postcss-plugin.ts +9 -6
- package/src/esbuild/esbuild-worker-plugin.ts +266 -0
- package/src/workers/client.worker.ts +4 -23
- package/src/workers/incremental-mtime-tracker.ts +68 -0
- package/src/workers/library-build.worker.ts +41 -14
- package/src/workers/server-build.worker.ts +6 -5
- package/src/workers/server-esbuild-context.ts +3 -1
- package/src/workers/server-watch-manager.ts +1 -1
- package/tests/angular/ngtsc-build-core.acc.spec.ts +210 -0
- package/tests/angular/ngtsc-build-core.spec.ts +52 -0
- package/tests/commands/version-upgrade.acc.spec.ts +210 -0
- package/tests/commands/version-upgrade.spec.ts +148 -0
- package/tests/deps/replace-deps/replace-deps-perf.verify.md +15 -0
- package/tests/deps/replace-deps/replace-deps-resolve.acc.spec.ts +124 -0
- package/tests/esbuild/esbuild-angular-compiler-plugin-worker.verify.md +56 -28
- package/tests/esbuild/esbuild-postcss-plugin-chunking.verify.md +17 -0
- package/tests/esbuild/esbuild-postcss-plugin.acc.spec.ts +152 -0
- package/tests/esbuild/esbuild-worker-plugin-node.verify.md +11 -0
- package/tests/esbuild/esbuild-worker-plugin.acc.spec.ts +318 -0
- package/tests/esbuild/esbuild-worker-plugin.spec.ts +297 -0
- package/tests/esbuild/esbuild-worker-plugin.verify.md +7 -0
- package/tests/esbuild/fixtures/worker-plugin/node-worker.js +2 -0
- package/tests/esbuild/fixtures/worker-plugin/shared-worker.js +6 -0
- package/tests/esbuild/fixtures/worker-plugin/worker-error.js +1 -0
- package/tests/esbuild/fixtures/worker-plugin/worker.js +3 -0
- package/tests/esbuild/fixtures/worker-plugin/worker2.js +3 -0
- package/tests/utils/ngtsc-build-core-write-emit.spec.ts +124 -0
- package/tests/workers/client-worker-mtime-incremental.verify.md +10 -0
- package/tests/workers/incremental-mtime-tracker.acc.spec.ts +144 -0
- package/tests/workers/incremental-mtime-tracker.spec.ts +102 -0
- package/tests/workers/library-build-worker.spec.ts +4 -0
- package/tests/workers/server-build-worker-plugin.verify.md +9 -0
- package/tests/workers/server-build-worker.spec.ts +26 -12
- package/tests/workers/server-esbuild-context.spec.ts +13 -5
- package/tests/workers/server-watch-manager.acc.spec.ts +2 -2
- package/tests/workers/server-watch-manager.spec.ts +2 -2
- package/dist/angular/web-worker-transformer.d.ts +0 -9
- package/dist/angular/web-worker-transformer.d.ts.map +0 -1
- package/dist/angular/web-worker-transformer.js +0 -73
- package/dist/angular/web-worker-transformer.js.map +0 -1
- package/src/angular/web-worker-transformer.ts +0 -117
- package/tests/angular/web-worker-transformer.spec.ts +0 -154
- package/tests/ts-compiler/fixtures/non-angular-pkg/dist/index.d.ts +0 -2
- package/tests/ts-compiler/fixtures/non-angular-pkg/dist/index.d.ts.map +0 -1
- package/tests/ts-compiler/fixtures/non-angular-pkg/dist/index.js +0 -4
- package/tests/ts-compiler/fixtures/non-angular-pkg/dist/index.js.map +0 -1
- package/tests/ts-compiler/fixtures/non-angular-pkg/dist/util.d.ts +0 -2
- package/tests/ts-compiler/fixtures/non-angular-pkg/dist/util.d.ts.map +0 -1
- package/tests/ts-compiler/fixtures/non-angular-pkg/dist/util.js +0 -4
- package/tests/ts-compiler/fixtures/non-angular-pkg/dist/util.js.map +0 -1
|
@@ -17,6 +17,7 @@ import { copyPublicFiles, watchPublicFiles } from "../utils/copy-public";
|
|
|
17
17
|
import type { SdBrowserSupportConfig, SdPwaConfig } from "../sd-config.types";
|
|
18
18
|
import type esbuild from "esbuild";
|
|
19
19
|
import type { PartialMessage } from "esbuild";
|
|
20
|
+
import { IncrementalMtimeTracker } from "./incremental-mtime-tracker";
|
|
20
21
|
|
|
21
22
|
//#region Types
|
|
22
23
|
|
|
@@ -194,32 +195,19 @@ function createSourceFileCachePlugin(): esbuild.Plugin {
|
|
|
194
195
|
return {
|
|
195
196
|
name: "sd-build-start",
|
|
196
197
|
setup(pluginBuild: esbuild.PluginBuild) {
|
|
197
|
-
const
|
|
198
|
+
const mtimeTracker = new IncrementalMtimeTracker();
|
|
198
199
|
|
|
199
200
|
pluginBuild.onStart(() => {
|
|
200
201
|
// sourceFileCache 무효화: 변경된 파일의 loadResultCache + TypeScript 소스 캐시 모두 제거
|
|
201
202
|
if (esbuildResult != null) {
|
|
202
203
|
const { loadResultCache, typeScriptFileCache } =
|
|
203
204
|
esbuildResult.sourceFileCache;
|
|
204
|
-
const changedFiles = new Set<string>();
|
|
205
205
|
// JS 파일 (loadResultCache) + TS 파일 (typeScriptFileCache) 모두 감시
|
|
206
206
|
const watchTargets = [
|
|
207
207
|
...loadResultCache.watchFiles,
|
|
208
208
|
...typeScriptFileCache.keys(),
|
|
209
209
|
];
|
|
210
|
-
|
|
211
|
-
try {
|
|
212
|
-
const mtime = fs.statSync(file).mtimeMs;
|
|
213
|
-
const prev = prevMtimes.get(file);
|
|
214
|
-
if (prev != null && prev !== mtime) {
|
|
215
|
-
changedFiles.add(file);
|
|
216
|
-
}
|
|
217
|
-
} catch {
|
|
218
|
-
if (prevMtimes.has(file)) {
|
|
219
|
-
changedFiles.add(file);
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
}
|
|
210
|
+
const changedFiles = mtimeTracker.detectChanges(watchTargets);
|
|
223
211
|
if (changedFiles.size > 0) {
|
|
224
212
|
esbuildResult.sourceFileCache.invalidate(changedFiles);
|
|
225
213
|
}
|
|
@@ -232,19 +220,12 @@ function createSourceFileCachePlugin(): esbuild.Plugin {
|
|
|
232
220
|
|
|
233
221
|
pluginBuild.onEnd(() => {
|
|
234
222
|
if (esbuildResult == null) return;
|
|
235
|
-
prevMtimes.clear();
|
|
236
223
|
// JS 파일 (loadResultCache) + TS 파일 (typeScriptFileCache) 모두 기록
|
|
237
224
|
const watchTargets = [
|
|
238
225
|
...esbuildResult.sourceFileCache.loadResultCache.watchFiles,
|
|
239
226
|
...esbuildResult.sourceFileCache.typeScriptFileCache.keys(),
|
|
240
227
|
];
|
|
241
|
-
|
|
242
|
-
try {
|
|
243
|
-
prevMtimes.set(file, fs.statSync(file).mtimeMs);
|
|
244
|
-
} catch {
|
|
245
|
-
// 삭제된 파일
|
|
246
|
-
}
|
|
247
|
-
}
|
|
228
|
+
mtimeTracker.updateMtimes(watchTargets);
|
|
248
229
|
});
|
|
249
230
|
},
|
|
250
231
|
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* 증분 방식으로 파일 mtime을 추적하여 변경 파일을 감지한다.
|
|
5
|
+
*
|
|
6
|
+
* - `detectChanges`: watchTargets 전체를 stat하여 이전 mtime과 비교, 변경 파일 Set 반환
|
|
7
|
+
* - `updateMtimes`: 변경/신규 파일만 stat하여 prevMtimes를 증분 갱신
|
|
8
|
+
*/
|
|
9
|
+
export class IncrementalMtimeTracker {
|
|
10
|
+
private readonly _prevMtimes = new Map<string, number>();
|
|
11
|
+
private _lastChangedFiles = new Set<string>();
|
|
12
|
+
|
|
13
|
+
detectChanges(watchTargets: Iterable<string>): Set<string> {
|
|
14
|
+
const changedFiles = new Set<string>();
|
|
15
|
+
for (const file of watchTargets) {
|
|
16
|
+
try {
|
|
17
|
+
const mtime = fs.statSync(file).mtimeMs;
|
|
18
|
+
const prev = this._prevMtimes.get(file);
|
|
19
|
+
if (prev != null && prev !== mtime) {
|
|
20
|
+
changedFiles.add(file);
|
|
21
|
+
}
|
|
22
|
+
} catch {
|
|
23
|
+
if (this._prevMtimes.has(file)) {
|
|
24
|
+
changedFiles.add(file);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
this._lastChangedFiles = changedFiles;
|
|
29
|
+
return changedFiles;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
updateMtimes(currentWatchTargets: Iterable<string>): void {
|
|
33
|
+
const targetSet =
|
|
34
|
+
currentWatchTargets instanceof Set
|
|
35
|
+
? (currentWatchTargets as Set<string>)
|
|
36
|
+
: new Set(currentWatchTargets);
|
|
37
|
+
|
|
38
|
+
// 1) 삭제된 파일 정리: prevMtimes에 있지만 watchTargets에 없는 파일 제거
|
|
39
|
+
for (const file of this._prevMtimes.keys()) {
|
|
40
|
+
if (!targetSet.has(file)) {
|
|
41
|
+
this._prevMtimes.delete(file);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 2) 변경된 파일 mtime 재조회 (watchTargets에 있는 파일만)
|
|
46
|
+
for (const file of this._lastChangedFiles) {
|
|
47
|
+
if (!targetSet.has(file)) continue;
|
|
48
|
+
try {
|
|
49
|
+
this._prevMtimes.set(file, fs.statSync(file).mtimeMs);
|
|
50
|
+
} catch {
|
|
51
|
+
this._prevMtimes.delete(file);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// 3) 신규 파일만 stat (prevMtimes에 없는 파일)
|
|
56
|
+
for (const file of targetSet) {
|
|
57
|
+
if (!this._prevMtimes.has(file)) {
|
|
58
|
+
try {
|
|
59
|
+
this._prevMtimes.set(file, fs.statSync(file).mtimeMs);
|
|
60
|
+
} catch {
|
|
61
|
+
// 삭제된 파일 — 무시
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
this._lastChangedFiles = new Set();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -7,7 +7,7 @@ import type { SerializedDiagnostic } from "../typecheck/typecheck-serialization"
|
|
|
7
7
|
import type { LintWithProgramResult } from "../lint/lint-with-program";
|
|
8
8
|
import { SdTsCompiler } from "../ts-compiler/SdTsCompiler";
|
|
9
9
|
import type { ISdTsCompilerResult } from "../ts-compiler/sd-ts-compiler-result";
|
|
10
|
-
import { writeEmitResults, compileSideEffectScss } from "../angular/ngtsc-build-core";
|
|
10
|
+
import { writeEmitResults, compileSideEffectScss, buildReverseDeps } from "../angular/ngtsc-build-core";
|
|
11
11
|
import { setupWorkerLifecycle } from "./shared-worker-lifecycle";
|
|
12
12
|
import { buildWatchPaths } from "./build-watch-paths";
|
|
13
13
|
import { hasFileAddOrRemove, shouldSkipRebuild } from "./build-change-filter";
|
|
@@ -46,6 +46,9 @@ export interface LibraryBuildWorkerEvents extends Record<string, unknown> {
|
|
|
46
46
|
let fsWatcher: FsWatcher | undefined;
|
|
47
47
|
let compiler: SdTsCompiler | undefined;
|
|
48
48
|
let combinedScssDeps = new Map<string, Set<string>>();
|
|
49
|
+
let reverseScssDeps = new Map<string, Set<string>>();
|
|
50
|
+
let registryReverseIndex = new Map<string, Set<string>>();
|
|
51
|
+
let sideEffectScssDeps = new Map<string, Set<string>>();
|
|
49
52
|
|
|
50
53
|
async function cleanup(): Promise<void> {
|
|
51
54
|
const watcherToClose = fsWatcher;
|
|
@@ -53,6 +56,9 @@ async function cleanup(): Promise<void> {
|
|
|
53
56
|
compiler = undefined;
|
|
54
57
|
lastSourceFilePaths = undefined;
|
|
55
58
|
combinedScssDeps = new Map();
|
|
59
|
+
reverseScssDeps = new Map();
|
|
60
|
+
registryReverseIndex = new Map();
|
|
61
|
+
sideEffectScssDeps = new Map();
|
|
56
62
|
await watcherToClose?.close();
|
|
57
63
|
}
|
|
58
64
|
|
|
@@ -93,13 +99,17 @@ async function build(info: LibraryBuildInfo): Promise<LibraryBuildResult> {
|
|
|
93
99
|
scssErrors: sideEffectScssErrors,
|
|
94
100
|
scssDependencies: combinedScssDeps,
|
|
95
101
|
registry: compiler.sideEffectScssRegistry,
|
|
102
|
+
registryReverseIndex,
|
|
103
|
+
sideEffectScssDeps,
|
|
96
104
|
});
|
|
97
|
-
// 초기 빌드: 등록된 side-effect SCSS 전체 컴파일
|
|
105
|
+
// 초기 빌드: 등록된 side-effect SCSS 전체 컴파일 (changedScssFiles 미전달)
|
|
98
106
|
compileSideEffectScss(
|
|
99
107
|
compiler.sideEffectScssRegistry,
|
|
100
108
|
loadPaths,
|
|
101
109
|
sideEffectScssErrors,
|
|
102
110
|
combinedScssDeps,
|
|
111
|
+
undefined,
|
|
112
|
+
sideEffectScssDeps,
|
|
103
113
|
);
|
|
104
114
|
} else {
|
|
105
115
|
writeEmitResults(filteredEmitResults, info.pkgDir);
|
|
@@ -147,12 +157,18 @@ function extractSourceFilePaths(program: ts.Program | undefined): Set<string> |
|
|
|
147
157
|
return paths;
|
|
148
158
|
}
|
|
149
159
|
|
|
150
|
-
/** compile-time SCSS 의존성으로 combinedScssDeps를 갱신한다 */
|
|
160
|
+
/** compile-time SCSS 의존성으로 combinedScssDeps와 reverseScssDeps를 갱신한다 */
|
|
151
161
|
function updateCombinedScssDeps(result: ISdTsCompilerResult): void {
|
|
152
162
|
combinedScssDeps = new Map();
|
|
153
163
|
for (const [file, deps] of result.scssDependencies) {
|
|
154
164
|
combinedScssDeps.set(file, new Set(deps));
|
|
155
165
|
}
|
|
166
|
+
rebuildReverseScssDeps();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** combinedScssDeps에서 역방향 인덱스를 재구축한다 */
|
|
170
|
+
function rebuildReverseScssDeps(): void {
|
|
171
|
+
reverseScssDeps = buildReverseDeps(combinedScssDeps);
|
|
156
172
|
}
|
|
157
173
|
|
|
158
174
|
/**
|
|
@@ -162,7 +178,7 @@ function updateCombinedScssDeps(result: ISdTsCompilerResult): void {
|
|
|
162
178
|
function buildWatchEvent(
|
|
163
179
|
info: LibraryBuildInfo,
|
|
164
180
|
result: ISdTsCompilerResult,
|
|
165
|
-
|
|
181
|
+
changedScssFiles?: ReadonlySet<string>,
|
|
166
182
|
): CombinedBuildEvent {
|
|
167
183
|
const isAngular = info.output.globalScss === true;
|
|
168
184
|
|
|
@@ -182,17 +198,25 @@ function buildWatchEvent(
|
|
|
182
198
|
scssErrors: sideEffectScssErrors,
|
|
183
199
|
scssDependencies: combinedScssDeps,
|
|
184
200
|
registry: compiler!.sideEffectScssRegistry,
|
|
201
|
+
registryReverseIndex,
|
|
202
|
+
sideEffectScssDeps,
|
|
185
203
|
},
|
|
186
204
|
);
|
|
187
205
|
|
|
188
|
-
//
|
|
189
|
-
|
|
206
|
+
// side-effect SCSS 재컴파일
|
|
207
|
+
// changedScssFiles 미제공 = 초기 빌드 → 전체 컴파일
|
|
208
|
+
// changedScssFiles 제공 + size > 0 → 증분 컴파일
|
|
209
|
+
if (changedScssFiles == null || changedScssFiles.size > 0) {
|
|
190
210
|
compileSideEffectScss(
|
|
191
211
|
compiler!.sideEffectScssRegistry,
|
|
192
212
|
loadPaths,
|
|
193
213
|
sideEffectScssErrors,
|
|
194
214
|
combinedScssDeps,
|
|
215
|
+
changedScssFiles,
|
|
216
|
+
sideEffectScssDeps,
|
|
195
217
|
);
|
|
218
|
+
// side-effect SCSS 의존성이 combinedScssDeps에 추가되었으므로 역방향 인덱스 재구축
|
|
219
|
+
rebuildReverseScssDeps();
|
|
196
220
|
}
|
|
197
221
|
|
|
198
222
|
// 모든 에러 통합
|
|
@@ -240,7 +264,7 @@ async function startWatch(info: LibraryBuildInfo): Promise<void> {
|
|
|
240
264
|
if (isAngular) {
|
|
241
265
|
updateCombinedScssDeps(initialResult);
|
|
242
266
|
}
|
|
243
|
-
const initialEvent = buildWatchEvent(info, initialResult
|
|
267
|
+
const initialEvent = buildWatchEvent(info, initialResult);
|
|
244
268
|
sender.send("build", initialEvent);
|
|
245
269
|
|
|
246
270
|
// workspace 의존성 경로 + replaceDeps 수집
|
|
@@ -265,12 +289,13 @@ async function startWatch(info: LibraryBuildInfo): Promise<void> {
|
|
|
265
289
|
for (const f of changedFiles) {
|
|
266
290
|
modifiedFiles.add(f.path);
|
|
267
291
|
|
|
268
|
-
// Angular: SCSS 역방향 의존성 탐색
|
|
292
|
+
// Angular: SCSS 역방향 의존성 탐색 (O(1) 조회)
|
|
269
293
|
if (isAngular && (f.path.endsWith(".scss") || f.path.endsWith(".css"))) {
|
|
270
294
|
const normalizedPath = pathx.posix(f.path);
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
295
|
+
const owners = reverseScssDeps.get(normalizedPath);
|
|
296
|
+
if (owners != null) {
|
|
297
|
+
for (const owner of owners) {
|
|
298
|
+
modifiedFiles.add(owner);
|
|
274
299
|
}
|
|
275
300
|
}
|
|
276
301
|
}
|
|
@@ -289,11 +314,13 @@ async function startWatch(info: LibraryBuildInfo): Promise<void> {
|
|
|
289
314
|
updateCombinedScssDeps(result);
|
|
290
315
|
}
|
|
291
316
|
|
|
292
|
-
const
|
|
293
|
-
|
|
317
|
+
const changedScssFiles = new Set(
|
|
318
|
+
changedFiles
|
|
319
|
+
.filter((f) => f.path.endsWith(".scss") || f.path.endsWith(".css"))
|
|
320
|
+
.map((f) => pathx.posix(f.path)),
|
|
294
321
|
);
|
|
295
322
|
|
|
296
|
-
const event = buildWatchEvent(info, result,
|
|
323
|
+
const event = buildWatchEvent(info, result, changedScssFiles);
|
|
297
324
|
sender.send("build", event);
|
|
298
325
|
} catch (err) {
|
|
299
326
|
sender.send("error", { message: errNs.message(err) });
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
import { collectAllExternals, generateProductionFiles } from "../deps/server-externals/server-production-files";
|
|
19
19
|
import { SdTsCompiler } from "../ts-compiler/SdTsCompiler";
|
|
20
20
|
import { createTscPlugin } from "../esbuild/esbuild-tsc-plugin";
|
|
21
|
+
import { createWorkerBundlePlugin } from "../esbuild/esbuild-worker-plugin";
|
|
21
22
|
import { setupWorkerLifecycle } from "./shared-worker-lifecycle";
|
|
22
23
|
import { buildWatchPaths } from "./build-watch-paths";
|
|
23
24
|
import { copyPublicFiles, watchPublicFiles } from "../utils/copy-public";
|
|
@@ -140,7 +141,7 @@ async function build(info: ServerBuildInfo): Promise<ServerBuildResult> {
|
|
|
140
141
|
const entryPoints = getPackageSourceFiles(info.pkgDir, parsedConfig);
|
|
141
142
|
|
|
142
143
|
// 외부 모듈 수집
|
|
143
|
-
const
|
|
144
|
+
const { bundleExternals, prodDependencies } = collectAllExternals(info.pkgDir, info.externals);
|
|
144
145
|
|
|
145
146
|
let jsResult: { success: boolean; errors?: string[]; warnings?: string[] };
|
|
146
147
|
let tscErrors: string[];
|
|
@@ -162,10 +163,10 @@ async function build(info: ServerBuildInfo): Promise<ServerBuildResult> {
|
|
|
162
163
|
pkgDir: info.pkgDir,
|
|
163
164
|
entryPoints,
|
|
164
165
|
env: info.env,
|
|
165
|
-
external,
|
|
166
|
+
external: bundleExternals,
|
|
166
167
|
});
|
|
167
168
|
|
|
168
|
-
jsResult = await esbuild.build({ ...esbuildOptions, plugins: [tscPlugin.plugin] })
|
|
169
|
+
jsResult = await esbuild.build({ ...esbuildOptions, plugins: [createWorkerBundlePlugin(), tscPlugin.plugin] })
|
|
169
170
|
.then(async (result) => {
|
|
170
171
|
if (result.outputFiles) {
|
|
171
172
|
await writeChangedOutputFiles(result.outputFiles);
|
|
@@ -212,7 +213,7 @@ async function build(info: ServerBuildInfo): Promise<ServerBuildResult> {
|
|
|
212
213
|
|
|
213
214
|
await copyPublicFiles(info.pkgDir, false);
|
|
214
215
|
|
|
215
|
-
generateProductionFiles(info,
|
|
216
|
+
generateProductionFiles(info, prodDependencies);
|
|
216
217
|
}
|
|
217
218
|
|
|
218
219
|
const allErrors = [...(jsResult.errors ?? []), ...tscErrors];
|
|
@@ -310,7 +311,7 @@ async function startWatch(info: ServerWatchInfo): Promise<void> {
|
|
|
310
311
|
const entryPoints = getPackageSourceFiles(info.pkgDir, parsedConfig);
|
|
311
312
|
|
|
312
313
|
// 외부 모듈 수집 (watch 모드용 — watch manager가 자체 캐시를 유지)
|
|
313
|
-
const cachedExternal = collectAllExternals(info.pkgDir, info.externals);
|
|
314
|
+
const { bundleExternals: cachedExternal } = collectAllExternals(info.pkgDir, info.externals);
|
|
314
315
|
|
|
315
316
|
// esbuild 컨텍스트 생성 (JS 출력 필요 시, tsc 플러그인 포함)
|
|
316
317
|
if (info.output.js) {
|
|
@@ -10,6 +10,7 @@ import { createTscPlugin, type TscPluginResult } from "../esbuild/esbuild-tsc-pl
|
|
|
10
10
|
import type { TypecheckEnv } from "../utils/tsconfig";
|
|
11
11
|
import type { SerializedDiagnostic } from "../typecheck/typecheck-serialization";
|
|
12
12
|
import type { LintWithProgramResult } from "../lint/lint-with-program";
|
|
13
|
+
import { createWorkerBundlePlugin } from "../esbuild/esbuild-worker-plugin";
|
|
13
14
|
|
|
14
15
|
/**
|
|
15
16
|
* esbuild watch context 생성 옵션
|
|
@@ -62,9 +63,10 @@ export async function createContext(options: EsbuildContextOptions): Promise<voi
|
|
|
62
63
|
dev: true,
|
|
63
64
|
});
|
|
64
65
|
|
|
66
|
+
const workerPlugin = createWorkerBundlePlugin();
|
|
65
67
|
context = await esbuild.context({
|
|
66
68
|
...baseOptions,
|
|
67
|
-
plugins: tscPlugin != null ? [tscPlugin.plugin] : [],
|
|
69
|
+
plugins: tscPlugin != null ? [workerPlugin, tscPlugin.plugin] : [workerPlugin],
|
|
68
70
|
metafile: true,
|
|
69
71
|
write: false,
|
|
70
72
|
});
|
|
@@ -59,7 +59,7 @@ export async function startServerWatchLoop(config: ServerWatchLoopConfig): Promi
|
|
|
59
59
|
c.path.endsWith("package.json"),
|
|
60
60
|
);
|
|
61
61
|
if (hasPackageJsonChange) {
|
|
62
|
-
cachedExternal = collectAllExternals(info.pkgDir, info.externals);
|
|
62
|
+
cachedExternal = collectAllExternals(info.pkgDir, info.externals).bundleExternals;
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
if (info.output.js) {
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import type { SideEffectScssEntry, SideEffectScssOptions } from "../../src/angular/ngtsc-build-core";
|
|
4
|
+
|
|
5
|
+
// Mock fs — filesystem I/O (OS 의존)
|
|
6
|
+
vi.mock("fs", () => ({
|
|
7
|
+
default: {
|
|
8
|
+
mkdirSync: vi.fn(),
|
|
9
|
+
writeFileSync: vi.fn(),
|
|
10
|
+
existsSync: vi.fn(() => false),
|
|
11
|
+
},
|
|
12
|
+
}));
|
|
13
|
+
|
|
14
|
+
// Mock scss-compiler — dart-sass 외부 의존성
|
|
15
|
+
const mockCompileScssFile = vi.fn<(filePath: string, loadPaths: string[]) => { css: string; dependencies: string[] }>()
|
|
16
|
+
.mockReturnValue({ css: "/* compiled */", dependencies: [] });
|
|
17
|
+
|
|
18
|
+
vi.mock("../../src/angular/scss-compiler", () => ({
|
|
19
|
+
compileScssFile: (filePath: string, loadPaths: string[]) => mockCompileScssFile(filePath, loadPaths),
|
|
20
|
+
compileScssString: vi.fn(() => ({ css: "", dependencies: [] })),
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
const { writeEmitResults, compileSideEffectScss } = await import("../../src/angular/ngtsc-build-core");
|
|
24
|
+
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
vi.clearAllMocks();
|
|
27
|
+
mockCompileScssFile.mockReturnValue({ css: "/* compiled */", dependencies: [] as string[] });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe("writeEmitResults — registryReverseIndex", () => {
|
|
31
|
+
// 테스트용 경로 헬퍼
|
|
32
|
+
const pkgDir = path.resolve("/test-pkg");
|
|
33
|
+
const srcFile = path.resolve(pkgDir, "src", "comp.ts");
|
|
34
|
+
const distDir = path.resolve(pkgDir, "dist");
|
|
35
|
+
|
|
36
|
+
function makeEmitResult(jsContent: string, sourceFileName?: string) {
|
|
37
|
+
// createOutputPathRewriter는 dist/ 하위 파일만 처리하므로 dist/ 경로로 생성
|
|
38
|
+
return {
|
|
39
|
+
filename: path.resolve(distDir, "comp.js"),
|
|
40
|
+
contents: jsContent,
|
|
41
|
+
sourceFileName,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Acceptance: registryReverseIndex가 제공되면 O(1) 삭제를 수행한다
|
|
46
|
+
it("deletes registry entries using reverseIndex and cleans up reverseIndex", () => {
|
|
47
|
+
const oldScssPath = path.resolve(pkgDir, "src", "old.scss");
|
|
48
|
+
const registry = new Map<string, SideEffectScssEntry>([
|
|
49
|
+
[oldScssPath, { scssAbsPath: oldScssPath, cssAbsPath: "/out/old.css", sourceFileName: srcFile }],
|
|
50
|
+
]);
|
|
51
|
+
const registryReverseIndex = new Map<string, Set<string>>([
|
|
52
|
+
[srcFile, new Set([oldScssPath])],
|
|
53
|
+
]);
|
|
54
|
+
const scss: SideEffectScssOptions = {
|
|
55
|
+
loadPaths: [],
|
|
56
|
+
scssErrors: [],
|
|
57
|
+
scssDependencies: new Map(),
|
|
58
|
+
registry,
|
|
59
|
+
registryReverseIndex,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// SCSS import 없는 JS → 삭제만 발생, 등록 없음
|
|
63
|
+
writeEmitResults([makeEmitResult("export class Comp {}", srcFile)], pkgDir, scss);
|
|
64
|
+
|
|
65
|
+
expect(registry.has(oldScssPath)).toBe(false);
|
|
66
|
+
expect(registryReverseIndex.has(srcFile)).toBe(false);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Acceptance: 삭제 후 새 SCSS import가 있으면 등록하고 reverseIndex도 갱신
|
|
70
|
+
it("registers new entries and updates reverseIndex after deletion", () => {
|
|
71
|
+
const oldScssPath = path.resolve(pkgDir, "src", "old.scss");
|
|
72
|
+
const registry = new Map<string, SideEffectScssEntry>([
|
|
73
|
+
[oldScssPath, { scssAbsPath: oldScssPath, cssAbsPath: "/out/old.css", sourceFileName: srcFile }],
|
|
74
|
+
]);
|
|
75
|
+
const registryReverseIndex = new Map<string, Set<string>>([
|
|
76
|
+
[srcFile, new Set([oldScssPath])],
|
|
77
|
+
]);
|
|
78
|
+
const scss: SideEffectScssOptions = {
|
|
79
|
+
loadPaths: [],
|
|
80
|
+
scssErrors: [],
|
|
81
|
+
scssDependencies: new Map(),
|
|
82
|
+
registry,
|
|
83
|
+
registryReverseIndex,
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// JS with SCSS import → 삭제 + 등록
|
|
87
|
+
const jsContent = 'import "./button.scss";\nexport class Comp {}';
|
|
88
|
+
writeEmitResults([makeEmitResult(jsContent, srcFile)], pkgDir, scss);
|
|
89
|
+
|
|
90
|
+
// old.scss 삭제됨
|
|
91
|
+
expect(registry.has(oldScssPath)).toBe(false);
|
|
92
|
+
// button.scss가 등록됨 (scssAbsPath = path.resolve(srcDir, "./button.scss"))
|
|
93
|
+
const expectedScssPath = path.resolve(pkgDir, "src", "button.scss");
|
|
94
|
+
expect(registry.has(expectedScssPath)).toBe(true);
|
|
95
|
+
// reverseIndex에 새 항목 반영
|
|
96
|
+
expect(registryReverseIndex.get(srcFile)).toEqual(new Set([expectedScssPath]));
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// Acceptance: reverseIndex에 없는 sourceFileName이면 삭제 없음
|
|
100
|
+
it("does not delete when sourceFileName is not in reverseIndex", () => {
|
|
101
|
+
const otherScssPath = path.resolve(pkgDir, "src", "other.scss");
|
|
102
|
+
const otherSrcFile = path.resolve(pkgDir, "src", "other.ts");
|
|
103
|
+
const registry = new Map<string, SideEffectScssEntry>([
|
|
104
|
+
[otherScssPath, { scssAbsPath: otherScssPath, cssAbsPath: "/out/other.css", sourceFileName: otherSrcFile }],
|
|
105
|
+
]);
|
|
106
|
+
const registryReverseIndex = new Map<string, Set<string>>([
|
|
107
|
+
[otherSrcFile, new Set([otherScssPath])],
|
|
108
|
+
]);
|
|
109
|
+
const scss: SideEffectScssOptions = {
|
|
110
|
+
loadPaths: [],
|
|
111
|
+
scssErrors: [],
|
|
112
|
+
scssDependencies: new Map(),
|
|
113
|
+
registry,
|
|
114
|
+
registryReverseIndex,
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// srcFile (comp.ts)에 대한 emit → other.ts의 항목은 건드리지 않음
|
|
118
|
+
writeEmitResults([makeEmitResult("export class Comp {}", srcFile)], pkgDir, scss);
|
|
119
|
+
|
|
120
|
+
expect(registry.has(otherScssPath)).toBe(true);
|
|
121
|
+
expect(registryReverseIndex.has(otherSrcFile)).toBe(true);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe("compileSideEffectScss — incremental compilation", () => {
|
|
126
|
+
function makeEntry(scssAbsPath: string, sourceFileName: string): SideEffectScssEntry {
|
|
127
|
+
return {
|
|
128
|
+
scssAbsPath,
|
|
129
|
+
cssAbsPath: scssAbsPath.replace(/\.scss$/, ".css"),
|
|
130
|
+
sourceFileName,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Acceptance: changedScssFiles 미제공 시 전체 재컴파일
|
|
135
|
+
it("compiles all entries when changedScssFiles is not provided", () => {
|
|
136
|
+
const registry = new Map<string, SideEffectScssEntry>([
|
|
137
|
+
["/src/button.scss", makeEntry("/src/button.scss", "/src/comp.ts")],
|
|
138
|
+
["/src/dialog.scss", makeEntry("/src/dialog.scss", "/src/comp.ts")],
|
|
139
|
+
["/src/card.scss", makeEntry("/src/card.scss", "/src/other.ts")],
|
|
140
|
+
]);
|
|
141
|
+
|
|
142
|
+
compileSideEffectScss(registry, [], [], new Map());
|
|
143
|
+
|
|
144
|
+
expect(mockCompileScssFile).toHaveBeenCalledTimes(3);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// Acceptance: 변경된 SCSS 파일만 재컴파일 (직접 히트)
|
|
148
|
+
it("only compiles entries whose scssAbsPath is in changedScssFiles", () => {
|
|
149
|
+
const registry = new Map<string, SideEffectScssEntry>([
|
|
150
|
+
["/src/button.scss", makeEntry("/src/button.scss", "/src/comp.ts")],
|
|
151
|
+
["/src/dialog.scss", makeEntry("/src/dialog.scss", "/src/other.ts")],
|
|
152
|
+
]);
|
|
153
|
+
const changedScssFiles = new Set(["/src/button.scss"]);
|
|
154
|
+
const sideEffectScssDeps = new Map<string, Set<string>>();
|
|
155
|
+
|
|
156
|
+
compileSideEffectScss(registry, [], [], new Map(), changedScssFiles, sideEffectScssDeps);
|
|
157
|
+
|
|
158
|
+
expect(mockCompileScssFile).toHaveBeenCalledTimes(1);
|
|
159
|
+
expect(mockCompileScssFile).toHaveBeenCalledWith("/src/button.scss", []);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// Acceptance: 의존성이 변경된 SCSS도 재컴파일
|
|
163
|
+
it("recompiles entries whose dependency is in changedScssFiles", () => {
|
|
164
|
+
const registry = new Map<string, SideEffectScssEntry>([
|
|
165
|
+
["/src/button.scss", makeEntry("/src/button.scss", "/src/comp.ts")],
|
|
166
|
+
]);
|
|
167
|
+
const changedScssFiles = new Set(["/src/shared.scss"]);
|
|
168
|
+
// button.scss의 이전 컴파일에서 shared.scss가 의존성이었음
|
|
169
|
+
const sideEffectScssDeps = new Map<string, Set<string>>([
|
|
170
|
+
["/src/button.scss", new Set(["/src/shared.scss"])],
|
|
171
|
+
]);
|
|
172
|
+
|
|
173
|
+
compileSideEffectScss(registry, [], [], new Map(), changedScssFiles, sideEffectScssDeps);
|
|
174
|
+
|
|
175
|
+
expect(mockCompileScssFile).toHaveBeenCalledTimes(1);
|
|
176
|
+
expect(mockCompileScssFile).toHaveBeenCalledWith("/src/button.scss", []);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// Acceptance: 영향받지 않는 항목은 건너뜀
|
|
180
|
+
it("skips entries not affected by changedScssFiles", () => {
|
|
181
|
+
const registry = new Map<string, SideEffectScssEntry>([
|
|
182
|
+
["/src/button.scss", makeEntry("/src/button.scss", "/src/comp.ts")],
|
|
183
|
+
["/src/dialog.scss", makeEntry("/src/dialog.scss", "/src/other.ts")],
|
|
184
|
+
]);
|
|
185
|
+
const changedScssFiles = new Set(["/src/shared.scss"]);
|
|
186
|
+
const sideEffectScssDeps = new Map<string, Set<string>>([
|
|
187
|
+
["/src/button.scss", new Set(["/src/shared.scss"])],
|
|
188
|
+
["/src/dialog.scss", new Set(["/src/theme.scss"])],
|
|
189
|
+
]);
|
|
190
|
+
|
|
191
|
+
compileSideEffectScss(registry, [], [], new Map(), changedScssFiles, sideEffectScssDeps);
|
|
192
|
+
|
|
193
|
+
// button.scss만 재컴파일 (shared.scss가 의존성), dialog.scss는 건너뜀
|
|
194
|
+
expect(mockCompileScssFile).toHaveBeenCalledTimes(1);
|
|
195
|
+
expect(mockCompileScssFile).toHaveBeenCalledWith("/src/button.scss", []);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// Unit: 컴파일 후 sideEffectScssDeps가 갱신됨
|
|
199
|
+
it("updates sideEffectScssDeps after compilation", () => {
|
|
200
|
+
const registry = new Map<string, SideEffectScssEntry>([
|
|
201
|
+
["/src/button.scss", makeEntry("/src/button.scss", "/src/comp.ts")],
|
|
202
|
+
]);
|
|
203
|
+
mockCompileScssFile.mockReturnValue({ css: "/* ok */", dependencies: ["/src/new-dep.scss"] });
|
|
204
|
+
const sideEffectScssDeps = new Map<string, Set<string>>();
|
|
205
|
+
|
|
206
|
+
compileSideEffectScss(registry, [], [], new Map(), undefined, sideEffectScssDeps);
|
|
207
|
+
|
|
208
|
+
expect(sideEffectScssDeps.get("/src/button.scss")).toEqual(new Set(["/src/new-dep.scss"]));
|
|
209
|
+
});
|
|
210
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { buildReverseDeps } from "../../src/angular/ngtsc-build-core";
|
|
3
|
+
|
|
4
|
+
describe("buildReverseDeps", () => {
|
|
5
|
+
// Acceptance: 정방향 맵에서 역방향 인덱스를 구축한다
|
|
6
|
+
it("builds reverse index from forward deps with multiple owners sharing a dep", () => {
|
|
7
|
+
const forward = new Map<string, ReadonlySet<string>>([
|
|
8
|
+
["comp.ts", new Set(["shared.scss", "theme.scss"])],
|
|
9
|
+
["dialog.ts", new Set(["shared.scss"])],
|
|
10
|
+
]);
|
|
11
|
+
|
|
12
|
+
const reverse = buildReverseDeps(forward);
|
|
13
|
+
|
|
14
|
+
expect(reverse.get("shared.scss")).toEqual(new Set(["comp.ts", "dialog.ts"]));
|
|
15
|
+
expect(reverse.get("theme.scss")).toEqual(new Set(["comp.ts"]));
|
|
16
|
+
expect(reverse.size).toBe(2);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// Acceptance: 빈 맵이면 역방향 맵도 비어있다
|
|
20
|
+
it("returns empty map for empty input", () => {
|
|
21
|
+
const reverse = buildReverseDeps(new Map());
|
|
22
|
+
|
|
23
|
+
expect(reverse.size).toBe(0);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
// Unit: 하나의 소유자가 여러 의존성을 가진 경우
|
|
27
|
+
it("maps each dep to its single owner", () => {
|
|
28
|
+
const forward = new Map<string, ReadonlySet<string>>([
|
|
29
|
+
["comp.ts", new Set(["a.scss", "b.scss", "c.scss"])],
|
|
30
|
+
]);
|
|
31
|
+
|
|
32
|
+
const reverse = buildReverseDeps(forward);
|
|
33
|
+
|
|
34
|
+
expect(reverse.size).toBe(3);
|
|
35
|
+
expect(reverse.get("a.scss")).toEqual(new Set(["comp.ts"]));
|
|
36
|
+
expect(reverse.get("b.scss")).toEqual(new Set(["comp.ts"]));
|
|
37
|
+
expect(reverse.get("c.scss")).toEqual(new Set(["comp.ts"]));
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Unit: 의존성이 빈 Set인 소유자는 역방향 맵에 영향 없음
|
|
41
|
+
it("ignores owners with empty dep sets", () => {
|
|
42
|
+
const forward = new Map<string, ReadonlySet<string>>([
|
|
43
|
+
["comp.ts", new Set<string>()],
|
|
44
|
+
["dialog.ts", new Set(["shared.scss"])],
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
const reverse = buildReverseDeps(forward);
|
|
48
|
+
|
|
49
|
+
expect(reverse.size).toBe(1);
|
|
50
|
+
expect(reverse.get("shared.scss")).toEqual(new Set(["dialog.ts"]));
|
|
51
|
+
});
|
|
52
|
+
});
|