@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
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import os from "os";
|
|
5
|
+
import { upgradeVersion, computePublishLevels } from "../../src/commands/publish/version-upgrade";
|
|
6
|
+
|
|
7
|
+
function createTempDir(): string {
|
|
8
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), "sd-cli-version-upgrade-"));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function writeJson(filePath: string, data: unknown): void {
|
|
12
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
13
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function readJson<T>(filePath: string): T {
|
|
17
|
+
return JSON.parse(fs.readFileSync(filePath, "utf-8")) as T;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe("upgradeVersion", () => {
|
|
21
|
+
let tmpDir: string;
|
|
22
|
+
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
tmpDir = createTempDir();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("복수 패키지의 package.json을 병렬 업데이트한다", async () => {
|
|
32
|
+
// Given: allPkgPaths에 5개 패키지 경로가 있다
|
|
33
|
+
writeJson(path.join(tmpDir, "package.json"), {
|
|
34
|
+
name: "@simplysm/root",
|
|
35
|
+
version: "14.0.0",
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const pkgNames = ["pkg-a", "pkg-b", "pkg-c", "pkg-d", "pkg-e"];
|
|
39
|
+
const allPkgPaths = pkgNames.map((name) => {
|
|
40
|
+
const pkgDir = path.join(tmpDir, "packages", name);
|
|
41
|
+
writeJson(path.join(pkgDir, "package.json"), {
|
|
42
|
+
name: `@simplysm/${name}`,
|
|
43
|
+
version: "14.0.0",
|
|
44
|
+
});
|
|
45
|
+
return pkgDir;
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// sd-cli/templates 디렉토리 생성 (glob 대상이 없도록)
|
|
49
|
+
fs.mkdirSync(path.join(tmpDir, "packages", "sd-cli", "templates"), {
|
|
50
|
+
recursive: true,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// When
|
|
54
|
+
const result = await upgradeVersion(tmpDir, allPkgPaths, false);
|
|
55
|
+
|
|
56
|
+
// Then: 5개 패키지의 package.json이 모두 newVersion으로 업데이트된다
|
|
57
|
+
expect(result.version).toBe("14.0.1");
|
|
58
|
+
for (const pkgDir of allPkgPaths) {
|
|
59
|
+
const pkg = readJson<{ version: string }>(path.join(pkgDir, "package.json"));
|
|
60
|
+
expect(pkg.version).toBe("14.0.1");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// And: changedFiles에 5개 패키지 경로가 모두 포함된다
|
|
64
|
+
for (const pkgDir of allPkgPaths) {
|
|
65
|
+
expect(result.changedFiles).toContain(path.resolve(pkgDir, "package.json"));
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it("패키지가 없으면 루트 package.json만 changedFiles에 포함된다", async () => {
|
|
70
|
+
// Given: allPkgPaths가 빈 배열이다
|
|
71
|
+
writeJson(path.join(tmpDir, "package.json"), {
|
|
72
|
+
name: "@simplysm/root",
|
|
73
|
+
version: "14.0.0",
|
|
74
|
+
});
|
|
75
|
+
fs.mkdirSync(path.join(tmpDir, "packages", "sd-cli", "templates"), {
|
|
76
|
+
recursive: true,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// When
|
|
80
|
+
const result = await upgradeVersion(tmpDir, [], false);
|
|
81
|
+
|
|
82
|
+
// Then: 루트 package.json만 changedFiles에 포함된다
|
|
83
|
+
expect(result.changedFiles).toHaveLength(1);
|
|
84
|
+
expect(result.changedFiles[0]).toBe(path.resolve(tmpDir, "package.json"));
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("복수 템플릿 파일을 병렬 업데이트한다", async () => {
|
|
88
|
+
// Given: 템플릿 3개 중 2개에 @simplysm 버전이 포함되어 있다
|
|
89
|
+
writeJson(path.join(tmpDir, "package.json"), {
|
|
90
|
+
name: "@simplysm/root",
|
|
91
|
+
version: "14.0.0",
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const templatesDir = path.join(tmpDir, "packages", "sd-cli", "templates");
|
|
95
|
+
fs.mkdirSync(templatesDir, { recursive: true });
|
|
96
|
+
|
|
97
|
+
// 템플릿 1: @simplysm 버전 포함
|
|
98
|
+
fs.writeFileSync(
|
|
99
|
+
path.join(templatesDir, "a.hbs"),
|
|
100
|
+
`"@simplysm/core-common": "~14.0.0"`,
|
|
101
|
+
"utf-8",
|
|
102
|
+
);
|
|
103
|
+
// 템플릿 2: @simplysm 버전 포함
|
|
104
|
+
fs.writeFileSync(
|
|
105
|
+
path.join(templatesDir, "b.hbs"),
|
|
106
|
+
`"@simplysm/angular": "~14.0.0"`,
|
|
107
|
+
"utf-8",
|
|
108
|
+
);
|
|
109
|
+
// 템플릿 3: @simplysm 버전 미포함
|
|
110
|
+
fs.writeFileSync(path.join(templatesDir, "c.hbs"), `no version here`, "utf-8");
|
|
111
|
+
|
|
112
|
+
// When
|
|
113
|
+
const result = await upgradeVersion(tmpDir, [], false);
|
|
114
|
+
|
|
115
|
+
// Then: 2개 파일만 수정되고 changedFiles에 추가된다
|
|
116
|
+
const templateChanges = result.changedFiles.filter((f) => f.endsWith(".hbs"));
|
|
117
|
+
expect(templateChanges).toHaveLength(2);
|
|
118
|
+
|
|
119
|
+
// 수정된 파일들의 내용 확인
|
|
120
|
+
expect(fs.readFileSync(path.join(templatesDir, "a.hbs"), "utf-8")).toContain("~14.0.1");
|
|
121
|
+
expect(fs.readFileSync(path.join(templatesDir, "b.hbs"), "utf-8")).toContain("~14.0.1");
|
|
122
|
+
// 수정되지 않은 파일
|
|
123
|
+
expect(fs.readFileSync(path.join(templatesDir, "c.hbs"), "utf-8")).toBe("no version here");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("템플릿이 없으면 템플릿 관련 쓰기 없이 완료된다", async () => {
|
|
127
|
+
// Given: glob 결과가 빈 배열이다 (templates 디렉토리에 .hbs 파일 없음)
|
|
128
|
+
writeJson(path.join(tmpDir, "package.json"), {
|
|
129
|
+
name: "@simplysm/root",
|
|
130
|
+
version: "14.0.0",
|
|
131
|
+
});
|
|
132
|
+
fs.mkdirSync(path.join(tmpDir, "packages", "sd-cli", "templates"), {
|
|
133
|
+
recursive: true,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// When
|
|
137
|
+
const result = await upgradeVersion(tmpDir, [], false);
|
|
138
|
+
|
|
139
|
+
// Then
|
|
140
|
+
const templateChanges = result.changedFiles.filter((f) => f.endsWith(".hbs"));
|
|
141
|
+
expect(templateChanges).toHaveLength(0);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it("changedFiles[0]이 프로젝트 루트 package.json 경로이다", async () => {
|
|
145
|
+
// Given: allPkgPaths에 3개 패키지가 있다
|
|
146
|
+
writeJson(path.join(tmpDir, "package.json"), {
|
|
147
|
+
name: "@simplysm/root",
|
|
148
|
+
version: "14.0.0",
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
const allPkgPaths = ["pkg-a", "pkg-b", "pkg-c"].map((name) => {
|
|
152
|
+
const pkgDir = path.join(tmpDir, "packages", name);
|
|
153
|
+
writeJson(path.join(pkgDir, "package.json"), {
|
|
154
|
+
name: `@simplysm/${name}`,
|
|
155
|
+
version: "14.0.0",
|
|
156
|
+
});
|
|
157
|
+
return pkgDir;
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
fs.mkdirSync(path.join(tmpDir, "packages", "sd-cli", "templates"), {
|
|
161
|
+
recursive: true,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// When
|
|
165
|
+
const result = await upgradeVersion(tmpDir, allPkgPaths, false);
|
|
166
|
+
|
|
167
|
+
// Then: changedFiles[0]이 프로젝트 루트 package.json 경로이다
|
|
168
|
+
expect(result.changedFiles[0]).toBe(path.resolve(tmpDir, "package.json"));
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe("computePublishLevels", () => {
|
|
173
|
+
let tmpDir: string;
|
|
174
|
+
|
|
175
|
+
beforeEach(() => {
|
|
176
|
+
tmpDir = createTempDir();
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
afterEach(() => {
|
|
180
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it("의존 관계가 있는 패키지들의 레벨을 올바르게 계산한다", async () => {
|
|
184
|
+
// Given: A(의존 없음), B(A 의존), C(B 의존)
|
|
185
|
+
const pkgs = [
|
|
186
|
+
{ name: "pkg-a", deps: {} },
|
|
187
|
+
{ name: "pkg-b", deps: { "@simplysm/pkg-a": "~14.0.0" } },
|
|
188
|
+
{ name: "pkg-c", deps: { "@simplysm/pkg-b": "~14.0.0" } },
|
|
189
|
+
];
|
|
190
|
+
|
|
191
|
+
const publishPkgs = pkgs.map((p) => {
|
|
192
|
+
const pkgDir = path.join(tmpDir, "packages", p.name);
|
|
193
|
+
writeJson(path.join(pkgDir, "package.json"), {
|
|
194
|
+
name: `@simplysm/${p.name}`,
|
|
195
|
+
version: "14.0.0",
|
|
196
|
+
dependencies: p.deps,
|
|
197
|
+
});
|
|
198
|
+
return { name: p.name, path: pkgDir };
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
// When
|
|
202
|
+
const levels = await computePublishLevels(publishPkgs);
|
|
203
|
+
|
|
204
|
+
// Then: 레벨은 [[A], [B], [C]]
|
|
205
|
+
expect(levels).toHaveLength(3);
|
|
206
|
+
expect(levels[0].map((p) => p.name)).toEqual(["pkg-a"]);
|
|
207
|
+
expect(levels[1].map((p) => p.name)).toEqual(["pkg-b"]);
|
|
208
|
+
expect(levels[2].map((p) => p.name)).toEqual(["pkg-c"]);
|
|
209
|
+
});
|
|
210
|
+
});
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import os from "os";
|
|
5
|
+
import { upgradeVersion, computePublishLevels } from "../../src/commands/publish/version-upgrade";
|
|
6
|
+
|
|
7
|
+
function createTempDir(): string {
|
|
8
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), "sd-cli-vu-unit-"));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function writeJson(filePath: string, data: unknown): void {
|
|
12
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
13
|
+
fs.writeFileSync(filePath, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function readJson<T>(filePath: string): T {
|
|
17
|
+
return JSON.parse(fs.readFileSync(filePath, "utf-8")) as T;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
describe("upgradeVersion — unit", () => {
|
|
21
|
+
let tmpDir: string;
|
|
22
|
+
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
tmpDir = createTempDir();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
afterEach(() => {
|
|
28
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("prerelease 버전은 prerelease로 증가한다", async () => {
|
|
32
|
+
writeJson(path.join(tmpDir, "package.json"), {
|
|
33
|
+
name: "@simplysm/root",
|
|
34
|
+
version: "14.0.0-beta.3",
|
|
35
|
+
});
|
|
36
|
+
fs.mkdirSync(path.join(tmpDir, "packages", "sd-cli", "templates"), {
|
|
37
|
+
recursive: true,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const result = await upgradeVersion(tmpDir, [], false);
|
|
41
|
+
expect(result.version).toBe("14.0.0-beta.4");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("dryRun이면 파일을 수정하지 않고 새 버전만 반환한다", async () => {
|
|
45
|
+
writeJson(path.join(tmpDir, "package.json"), {
|
|
46
|
+
name: "@simplysm/root",
|
|
47
|
+
version: "14.0.0",
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const pkgDir = path.join(tmpDir, "packages", "pkg-a");
|
|
51
|
+
writeJson(path.join(pkgDir, "package.json"), {
|
|
52
|
+
name: "@simplysm/pkg-a",
|
|
53
|
+
version: "14.0.0",
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const result = await upgradeVersion(tmpDir, [pkgDir], true);
|
|
57
|
+
expect(result.version).toBe("14.0.1");
|
|
58
|
+
expect(result.changedFiles).toHaveLength(0);
|
|
59
|
+
|
|
60
|
+
// 파일이 수정되지 않았는지 확인
|
|
61
|
+
const rootPkg = readJson<{ version: string }>(path.join(tmpDir, "package.json"));
|
|
62
|
+
expect(rootPkg.version).toBe("14.0.0");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("템플릿 파일에서 @simplysm 버전이 없으면 수정하지 않는다", async () => {
|
|
66
|
+
writeJson(path.join(tmpDir, "package.json"), {
|
|
67
|
+
name: "@simplysm/root",
|
|
68
|
+
version: "14.0.0",
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const templatesDir = path.join(tmpDir, "packages", "sd-cli", "templates");
|
|
72
|
+
fs.mkdirSync(templatesDir, { recursive: true });
|
|
73
|
+
fs.writeFileSync(path.join(templatesDir, "no-version.hbs"), "plain template content", "utf-8");
|
|
74
|
+
|
|
75
|
+
const result = await upgradeVersion(tmpDir, [], false);
|
|
76
|
+
const templateChanges = result.changedFiles.filter((f) => f.endsWith(".hbs"));
|
|
77
|
+
expect(templateChanges).toHaveLength(0);
|
|
78
|
+
|
|
79
|
+
// 파일이 수정되지 않았는지 확인
|
|
80
|
+
expect(fs.readFileSync(path.join(templatesDir, "no-version.hbs"), "utf-8")).toBe(
|
|
81
|
+
"plain template content",
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe("computePublishLevels — unit", () => {
|
|
87
|
+
let tmpDir: string;
|
|
88
|
+
|
|
89
|
+
beforeEach(() => {
|
|
90
|
+
tmpDir = createTempDir();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
afterEach(() => {
|
|
94
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("의존성 없는 패키지들은 모두 Level 0이다", async () => {
|
|
98
|
+
const publishPkgs = ["pkg-a", "pkg-b", "pkg-c"].map((name) => {
|
|
99
|
+
const pkgDir = path.join(tmpDir, "packages", name);
|
|
100
|
+
writeJson(path.join(pkgDir, "package.json"), {
|
|
101
|
+
name: `@simplysm/${name}`,
|
|
102
|
+
version: "14.0.0",
|
|
103
|
+
dependencies: {},
|
|
104
|
+
});
|
|
105
|
+
return { name, path: pkgDir };
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const levels = await computePublishLevels(publishPkgs);
|
|
109
|
+
expect(levels).toHaveLength(1);
|
|
110
|
+
expect(levels[0].map((p) => p.name).sort()).toEqual(["pkg-a", "pkg-b", "pkg-c"]);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("외부 의존성(@simplysm/ 아닌)은 레벨 계산에 영향 없다", async () => {
|
|
114
|
+
const pkgDir = path.join(tmpDir, "packages", "pkg-a");
|
|
115
|
+
writeJson(path.join(pkgDir, "package.json"), {
|
|
116
|
+
name: "@simplysm/pkg-a",
|
|
117
|
+
version: "14.0.0",
|
|
118
|
+
dependencies: { lodash: "^4.0.0", express: "^5.0.0" },
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const levels = await computePublishLevels([{ name: "pkg-a", path: pkgDir }]);
|
|
122
|
+
expect(levels).toHaveLength(1);
|
|
123
|
+
expect(levels[0][0].name).toBe("pkg-a");
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it("peerDependencies와 optionalDependencies도 레벨에 반영된다", async () => {
|
|
127
|
+
const pkgADir = path.join(tmpDir, "packages", "pkg-a");
|
|
128
|
+
writeJson(path.join(pkgADir, "package.json"), {
|
|
129
|
+
name: "@simplysm/pkg-a",
|
|
130
|
+
version: "14.0.0",
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const pkgBDir = path.join(tmpDir, "packages", "pkg-b");
|
|
134
|
+
writeJson(path.join(pkgBDir, "package.json"), {
|
|
135
|
+
name: "@simplysm/pkg-b",
|
|
136
|
+
version: "14.0.0",
|
|
137
|
+
peerDependencies: { "@simplysm/pkg-a": "~14.0.0" },
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
const levels = await computePublishLevels([
|
|
141
|
+
{ name: "pkg-a", path: pkgADir },
|
|
142
|
+
{ name: "pkg-b", path: pkgBDir },
|
|
143
|
+
]);
|
|
144
|
+
expect(levels).toHaveLength(2);
|
|
145
|
+
expect(levels[0][0].name).toBe("pkg-a");
|
|
146
|
+
expect(levels[1][0].name).toBe("pkg-b");
|
|
147
|
+
});
|
|
148
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# replace-deps 성능 최적화 — LLM 검증
|
|
2
|
+
|
|
3
|
+
## 검증 항목
|
|
4
|
+
|
|
5
|
+
### Slice 1: Set 중복 검사 + glob 병렬화
|
|
6
|
+
|
|
7
|
+
- [x] `resolveAllReplaceDepEntries`에서 `entries.some()` 대신 `Set<string>` 사용 확인: `replace-deps-resolve.ts:143`에 `seenTargetPaths = new Set<string>()` 선언, `:205`에서 `seenTargetPaths.has(actualTargetPath)` + `seenTargetPaths.add(actualTargetPath)` 사용. 기존 `entries.some()` 제거됨.
|
|
8
|
+
- [x] glob 병렬화 확인: `replace-deps-resolve.ts:167-171`에서 `Promise.all(Object.keys(replaceDeps).map(...))` 사용. 기존 순차 `for` 루프 제거됨. `:172-174`에서 `flatMap`으로 결과를 합침.
|
|
9
|
+
- [x] `seenTargetPaths`가 while 루프 바깥에 선언되어 재귀 탐색 전체에 걸쳐 중복 방지가 유지됨 확인.
|
|
10
|
+
|
|
11
|
+
### Slice 2: watch onChange entries 사전 필터링
|
|
12
|
+
|
|
13
|
+
- [x] `watchReplaceDeps`에서 사전 필터링 확인: `replace-deps.ts:240-242`에 `sourceEntries = entries.filter(e => e.resolvedSourcePath === entry.resolvedSourcePath)` 선언. watcher 생성 전에 필터링되어 클로저에 캡처됨.
|
|
14
|
+
- [x] onChange 내부에서 `sourceEntries`만 순회 확인: `replace-deps.ts:252`에서 `for (const e of sourceEntries)` 사용. 기존 `for (const e of entries)` + `if (e.resolvedSourcePath !== entry.resolvedSourcePath) continue;` 패턴이 제거됨.
|
|
15
|
+
- [x] `entries` 배열은 `resolveAllReplaceDepEntries` 호출 후 불변이므로, 필터링 결과가 watcher 생명주기 동안 유효함 확인 (`replace-deps.ts:202`에서 `entries` 할당 후 수정 없음).
|