@simplysm/sd-cli 14.0.43 → 14.0.44
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/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/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/package.json +4 -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/esbuild/esbuild-postcss-plugin.ts +9 -6
- 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/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-postcss-plugin-chunking.verify.md +17 -0
- package/tests/esbuild/esbuild-postcss-plugin.acc.spec.ts +152 -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/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
|
@@ -46,28 +46,35 @@ export async function upgradeVersion(
|
|
|
46
46
|
await fsx.write(projPkgPath, json.stringify(projPkg, { space: 2 }) + "\n");
|
|
47
47
|
changedFiles.push(projPkgPath);
|
|
48
48
|
|
|
49
|
-
// 각 패키지의 package.json에 버전 설정
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
49
|
+
// 각 패키지의 package.json에 버전 설정 (병렬)
|
|
50
|
+
const pkgChangedFiles = await Promise.all(
|
|
51
|
+
allPkgPaths.map(async (pkgPath) => {
|
|
52
|
+
const pkgJsonPath = path.resolve(pkgPath, "package.json");
|
|
53
|
+
const pkgJson = await fsx.readJson<PackageJson>(pkgJsonPath);
|
|
54
|
+
pkgJson.version = newVersion;
|
|
55
|
+
await fsx.write(pkgJsonPath, json.stringify(pkgJson, { space: 2 }) + "\n");
|
|
56
|
+
return pkgJsonPath;
|
|
57
|
+
}),
|
|
58
|
+
);
|
|
59
|
+
changedFiles.push(...pkgChangedFiles);
|
|
57
60
|
|
|
58
61
|
// 템플릿 파일의 @simplysm 패키지 버전 동기화
|
|
59
62
|
const templateFiles = await fsx.glob(path.resolve(cwd, "packages/sd-cli/templates/**/*.hbs"));
|
|
60
63
|
const versionRegex = /("@simplysm\/[^"]+"\s*:\s*)"~[^"]+"/g;
|
|
61
64
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
+
const templateChangedFiles = await Promise.all(
|
|
66
|
+
templateFiles.map(async (templatePath) => {
|
|
67
|
+
const content = await fsx.read(templatePath);
|
|
68
|
+
const newContent = content.replace(versionRegex, `$1"~${newVersion}"`);
|
|
65
69
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
70
|
+
if (content !== newContent) {
|
|
71
|
+
await fsx.write(templatePath, newContent);
|
|
72
|
+
return templatePath;
|
|
73
|
+
}
|
|
74
|
+
return undefined;
|
|
75
|
+
}),
|
|
76
|
+
);
|
|
77
|
+
changedFiles.push(...templateChangedFiles.filter((f) => f != null));
|
|
71
78
|
|
|
72
79
|
return { version: newVersion, changedFiles };
|
|
73
80
|
}
|
|
@@ -81,25 +88,27 @@ export async function computePublishLevels<T extends { name: string; path: strin
|
|
|
81
88
|
): Promise<T[][]> {
|
|
82
89
|
const pkgNames = new Set(publishPkgs.map((p) => p.name));
|
|
83
90
|
|
|
84
|
-
// 각 패키지의 워크스페이스 의존성 수집
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
91
|
+
// 각 패키지의 워크스페이스 의존성 수집 (병렬)
|
|
92
|
+
const depsEntries = await Promise.all(
|
|
93
|
+
publishPkgs.map(async (pkg) => {
|
|
94
|
+
const pkgJson = await fsx.readJson<PackageJson>(path.resolve(pkg.path, "package.json"));
|
|
95
|
+
const allDeps = {
|
|
96
|
+
...pkgJson.dependencies,
|
|
97
|
+
...pkgJson.peerDependencies,
|
|
98
|
+
...pkgJson.optionalDependencies,
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const workspaceDeps = new Set<string>();
|
|
102
|
+
for (const depName of Object.keys(allDeps)) {
|
|
103
|
+
const shortName = depName.replace(/^@simplysm\//, "");
|
|
104
|
+
if (shortName !== depName && pkgNames.has(shortName)) {
|
|
105
|
+
workspaceDeps.add(shortName);
|
|
106
|
+
}
|
|
99
107
|
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
108
|
+
return [pkg.name, workspaceDeps] as const;
|
|
109
|
+
}),
|
|
110
|
+
);
|
|
111
|
+
const depsMap = new Map(depsEntries);
|
|
103
112
|
|
|
104
113
|
// 위상 정렬로 레벨 분류
|
|
105
114
|
const levels: T[][] = [];
|
|
@@ -140,6 +140,7 @@ export async function resolveAllReplaceDepEntries(
|
|
|
140
140
|
logger: ReturnType<typeof consola.withTag>,
|
|
141
141
|
): Promise<ReplaceDepEntry[]> {
|
|
142
142
|
const entries: ReplaceDepEntry[] = [];
|
|
143
|
+
const seenTargetPaths = new Set<string>();
|
|
143
144
|
const searchedDirs = new Set<string>();
|
|
144
145
|
|
|
145
146
|
// 초기 탐색 대상: 프로젝트 루트 + workspace 패키지들의 node_modules
|
|
@@ -162,12 +163,15 @@ export async function resolveAllReplaceDepEntries(
|
|
|
162
163
|
continue;
|
|
163
164
|
}
|
|
164
165
|
|
|
165
|
-
// replaceDeps의 각 glob 패턴으로 node_modules 디렉토리 탐색
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
166
|
+
// replaceDeps의 각 glob 패턴으로 node_modules 디렉토리 탐색 (병렬)
|
|
167
|
+
const globResults = await Promise.all(
|
|
168
|
+
Object.keys(replaceDeps).map((pattern) =>
|
|
169
|
+
glob(pattern, { cwd: nodeModulesDir }),
|
|
170
|
+
),
|
|
171
|
+
);
|
|
172
|
+
const targetNames = globResults.flatMap((matches) =>
|
|
173
|
+
matches.map((m) => m.replaceAll("\\", "/")),
|
|
174
|
+
);
|
|
171
175
|
|
|
172
176
|
logger.debug(`[replace-deps] 탐색: ${nodeModulesDir} → ${targetNames.length}개 매칭 (${targetNames.join(", ")})`);
|
|
173
177
|
|
|
@@ -200,7 +204,8 @@ export async function resolveAllReplaceDepEntries(
|
|
|
200
204
|
}
|
|
201
205
|
|
|
202
206
|
// 동일 actualTargetPath가 이미 등록된 경우 건너뜀 (pnpm 중복 방지)
|
|
203
|
-
if (
|
|
207
|
+
if (seenTargetPaths.has(actualTargetPath)) continue;
|
|
208
|
+
seenTargetPaths.add(actualTargetPath);
|
|
204
209
|
|
|
205
210
|
entries.push({
|
|
206
211
|
targetName,
|
|
@@ -7,6 +7,70 @@ import { promisify } from "util";
|
|
|
7
7
|
import type { ReplaceDepEntry } from "./replace-deps-resolve";
|
|
8
8
|
import { resolveAllReplaceDepEntries } from "./replace-deps-resolve";
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* 파일 내용이 동일한지 비교한다.
|
|
12
|
+
* mtime + size가 같으면 동일로 간주하고, 다르면 바이트 비교한다.
|
|
13
|
+
*/
|
|
14
|
+
async function isFileContentSame(pathA: string, pathB: string): Promise<boolean> {
|
|
15
|
+
try {
|
|
16
|
+
const [statA, statB] = await Promise.all([
|
|
17
|
+
fs.promises.stat(pathA),
|
|
18
|
+
fs.promises.stat(pathB),
|
|
19
|
+
]);
|
|
20
|
+
if (statA.size !== statB.size) return false;
|
|
21
|
+
if (statA.mtimeMs === statB.mtimeMs) return true;
|
|
22
|
+
|
|
23
|
+
const [bufA, bufB] = await Promise.all([
|
|
24
|
+
fs.promises.readFile(pathA),
|
|
25
|
+
fs.promises.readFile(pathB),
|
|
26
|
+
]);
|
|
27
|
+
return bufA.equals(bufB);
|
|
28
|
+
} catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* pnpm hard link를 끊으면서 파일/디렉토리를 복사한다.
|
|
35
|
+
* 대상 파일을 먼저 unlink하여 글로벌 store의 hard link를 끊고 새 파일을 생성한다.
|
|
36
|
+
* 다른 프로젝트의 node_modules에 영향을 주지 않기 위함이다.
|
|
37
|
+
*/
|
|
38
|
+
async function copyWithUnlink(
|
|
39
|
+
sourcePath: string,
|
|
40
|
+
targetPath: string,
|
|
41
|
+
filter?: (absolutePath: string) => boolean,
|
|
42
|
+
): Promise<void> {
|
|
43
|
+
let stats: fs.Stats;
|
|
44
|
+
try {
|
|
45
|
+
stats = await fs.promises.lstat(sourcePath);
|
|
46
|
+
} catch {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (stats.isDirectory()) {
|
|
51
|
+
await fsx.mkdir(targetPath);
|
|
52
|
+
const names = await fs.promises.readdir(sourcePath);
|
|
53
|
+
await Promise.all(
|
|
54
|
+
names
|
|
55
|
+
.map((name) => path.resolve(sourcePath, name))
|
|
56
|
+
.filter((child) => filter == null || filter(child))
|
|
57
|
+
.map((child) => copyWithUnlink(
|
|
58
|
+
child,
|
|
59
|
+
path.join(targetPath, path.basename(child)),
|
|
60
|
+
)),
|
|
61
|
+
);
|
|
62
|
+
} else {
|
|
63
|
+
if (await isFileContentSame(sourcePath, targetPath)) return;
|
|
64
|
+
await fsx.mkdir(path.dirname(targetPath));
|
|
65
|
+
try {
|
|
66
|
+
await fs.promises.unlink(targetPath);
|
|
67
|
+
} catch {
|
|
68
|
+
// 대상이 없으면 무시
|
|
69
|
+
}
|
|
70
|
+
await fs.promises.copyFile(sourcePath, targetPath);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
10
74
|
/**
|
|
11
75
|
* npm publish 시 files 필드와 무관하게 항상 포함되는 파일 패턴 (대소문자 무시)
|
|
12
76
|
*/
|
|
@@ -69,33 +133,31 @@ export async function setupReplaceDeps(
|
|
|
69
133
|
replaceDeps: Record<string, string>,
|
|
70
134
|
): Promise<void> {
|
|
71
135
|
const logger = consola.withTag("sd:cli:replace-deps");
|
|
72
|
-
let setupCount = 0;
|
|
73
136
|
|
|
74
137
|
logger.start("replace-deps 설정 중...");
|
|
75
138
|
|
|
76
139
|
const entries = await resolveAllReplaceDepEntries(projectRoot, replaceDeps, logger);
|
|
77
|
-
const copiedEntries: ReplaceDepEntry[] = [];
|
|
78
140
|
|
|
79
|
-
|
|
141
|
+
const results = await Promise.all(entries.map(async (entry) => {
|
|
80
142
|
try {
|
|
81
|
-
// 소스 패키지의 files 필드를 읽어 화이트리스트 구성
|
|
82
143
|
const files = await loadFilesField(entry.resolvedSourcePath);
|
|
83
144
|
if (files == null) {
|
|
84
145
|
logger.warn(`[${entry.targetName}] package.json에 files 필드가 없어 건너뜀`);
|
|
85
|
-
|
|
146
|
+
return undefined;
|
|
86
147
|
}
|
|
87
148
|
|
|
88
149
|
const filter = createCopyFilter(entry.resolvedSourcePath, new Set(files));
|
|
89
|
-
await
|
|
90
|
-
|
|
91
|
-
copiedEntries.push(entry);
|
|
92
|
-
setupCount += 1;
|
|
150
|
+
await copyWithUnlink(entry.resolvedSourcePath, entry.actualTargetPath, filter);
|
|
151
|
+
return entry;
|
|
93
152
|
} catch (err) {
|
|
94
153
|
logger.error(`[${entry.targetName}] 복사 실패: ${err instanceof Error ? err.message : err}`);
|
|
154
|
+
return undefined;
|
|
95
155
|
}
|
|
96
|
-
}
|
|
156
|
+
}));
|
|
97
157
|
|
|
98
|
-
|
|
158
|
+
const copiedEntries = results.filter((e): e is ReplaceDepEntry => e != null);
|
|
159
|
+
|
|
160
|
+
logger.success(`replace-deps 설정 완료 (${copiedEntries.length}개 의존성 교체)`);
|
|
99
161
|
|
|
100
162
|
// 교체된 패키지의 postinstall 스크립트 실행
|
|
101
163
|
for (const { targetName, resolvedSourcePath, actualTargetPath } of copiedEntries) {
|
|
@@ -174,15 +236,20 @@ export async function watchReplaceDeps(
|
|
|
174
236
|
// readdir 실패 시 npm 기본 파일 감시 생략
|
|
175
237
|
}
|
|
176
238
|
|
|
239
|
+
// 이 소스 경로에 해당하는 entries만 사전 필터링하여 캡처
|
|
240
|
+
const sourceEntries = entries.filter(
|
|
241
|
+
(e) => e.resolvedSourcePath === entry.resolvedSourcePath,
|
|
242
|
+
);
|
|
243
|
+
|
|
177
244
|
const watcher = await FsWatcher.watch(watchPaths, {
|
|
178
245
|
followSymlinks: false,
|
|
179
246
|
});
|
|
180
247
|
watcher.onChange({ delay: 300 }, async (changeInfos) => {
|
|
181
|
-
|
|
182
|
-
// 이 소스 경로를 사용하는 모든 항목에 대해 복사
|
|
183
|
-
for (const e of entries) {
|
|
184
|
-
if (e.resolvedSourcePath !== entry.resolvedSourcePath) continue;
|
|
248
|
+
let hasActualCopy = false;
|
|
185
249
|
|
|
250
|
+
for (const { path: changedPath } of changeInfos) {
|
|
251
|
+
// 사전 필터링된 항목만 순회
|
|
252
|
+
for (const e of sourceEntries) {
|
|
186
253
|
// 소스로부터의 상대 경로 계산
|
|
187
254
|
const relativePath = pathx.posix(path.relative(e.resolvedSourcePath, changedPath));
|
|
188
255
|
const destPath = pathx.posix(path.join(e.actualTargetPath, relativePath));
|
|
@@ -203,12 +270,16 @@ export async function watchReplaceDeps(
|
|
|
203
270
|
if (stat.isDirectory()) {
|
|
204
271
|
await fsx.mkdir(destPath);
|
|
205
272
|
} else {
|
|
273
|
+
// 파일 내용이 동일하면 복사 건너뜀 (불필요한 리빌드 방지)
|
|
274
|
+
if (await isFileContentSame(changedPath, destPath)) continue;
|
|
206
275
|
await fsx.mkdir(pathx.posix(path.dirname(destPath)));
|
|
207
276
|
await fsx.copy(changedPath, destPath);
|
|
277
|
+
hasActualCopy = true;
|
|
208
278
|
}
|
|
209
279
|
} else {
|
|
210
280
|
// 소스가 삭제됨 → 대상도 삭제
|
|
211
281
|
await fsx.rm(destPath);
|
|
282
|
+
hasActualCopy = true;
|
|
212
283
|
}
|
|
213
284
|
} catch (err) {
|
|
214
285
|
logger.error(
|
|
@@ -217,7 +288,10 @@ export async function watchReplaceDeps(
|
|
|
217
288
|
}
|
|
218
289
|
}
|
|
219
290
|
}
|
|
220
|
-
|
|
291
|
+
|
|
292
|
+
if (hasActualCopy) {
|
|
293
|
+
options?.onChanged?.();
|
|
294
|
+
}
|
|
221
295
|
});
|
|
222
296
|
|
|
223
297
|
watchers.push(watcher);
|
|
@@ -88,16 +88,19 @@ export function createPostcssPlugin(options: CreatePostcssPluginOptions): esbuil
|
|
|
88
88
|
|
|
89
89
|
if (replacements.length === 0) continue;
|
|
90
90
|
|
|
91
|
-
// PostCSS 적용
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
91
|
+
// PostCSS 적용 — 정방향 청크 배열 + join
|
|
92
|
+
const sorted = replacements.sort((a, b) => a.start - b.start);
|
|
93
|
+
const chunks: string[] = [];
|
|
94
|
+
let cursor = 0;
|
|
95
95
|
|
|
96
96
|
for (const rep of sorted) {
|
|
97
|
+
chunks.push(code.slice(cursor, rep.start));
|
|
97
98
|
const processed = await processor.process(rep.text, { from: file });
|
|
98
|
-
|
|
99
|
-
|
|
99
|
+
chunks.push(JSON.stringify(processed.css));
|
|
100
|
+
cursor = rep.end;
|
|
100
101
|
}
|
|
102
|
+
chunks.push(code.slice(cursor));
|
|
103
|
+
const modified = chunks.join("");
|
|
101
104
|
|
|
102
105
|
await fs.promises.writeFile(file, modified);
|
|
103
106
|
} catch (e: unknown) {
|
|
@@ -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) });
|