@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.
Files changed (103) hide show
  1. package/dist/angular/ngtsc-build-core.d.ts +12 -3
  2. package/dist/angular/ngtsc-build-core.d.ts.map +1 -1
  3. package/dist/angular/ngtsc-build-core.js +70 -6
  4. package/dist/angular/ngtsc-build-core.js.map +1 -1
  5. package/dist/commands/publish/version-upgrade.d.ts.map +1 -1
  6. package/dist/commands/publish/version-upgrade.js +15 -12
  7. package/dist/commands/publish/version-upgrade.js.map +1 -1
  8. package/dist/deps/replace-deps/replace-deps-resolve.d.ts.map +1 -1
  9. package/dist/deps/replace-deps/replace-deps-resolve.js +6 -7
  10. package/dist/deps/replace-deps/replace-deps-resolve.js.map +1 -1
  11. package/dist/deps/replace-deps/replace-deps.d.ts.map +1 -1
  12. package/dist/deps/replace-deps/replace-deps.js +79 -15
  13. package/dist/deps/replace-deps/replace-deps.js.map +1 -1
  14. package/dist/deps/server-externals/server-production-files.d.ts +10 -5
  15. package/dist/deps/server-externals/server-production-files.d.ts.map +1 -1
  16. package/dist/deps/server-externals/server-production-files.js +22 -26
  17. package/dist/deps/server-externals/server-production-files.js.map +1 -1
  18. package/dist/esbuild/esbuild-angular-compiler-plugin.d.ts +3 -8
  19. package/dist/esbuild/esbuild-angular-compiler-plugin.d.ts.map +1 -1
  20. package/dist/esbuild/esbuild-angular-compiler-plugin.js +57 -83
  21. package/dist/esbuild/esbuild-angular-compiler-plugin.js.map +1 -1
  22. package/dist/esbuild/esbuild-postcss-plugin.d.ts.map +1 -1
  23. package/dist/esbuild/esbuild-postcss-plugin.js +9 -6
  24. package/dist/esbuild/esbuild-postcss-plugin.js.map +1 -1
  25. package/dist/esbuild/esbuild-worker-plugin.d.ts +30 -0
  26. package/dist/esbuild/esbuild-worker-plugin.d.ts.map +1 -0
  27. package/dist/esbuild/esbuild-worker-plugin.js +197 -0
  28. package/dist/esbuild/esbuild-worker-plugin.js.map +1 -0
  29. package/dist/workers/client.worker.d.ts.map +1 -1
  30. package/dist/workers/client.worker.js +4 -25
  31. package/dist/workers/client.worker.js.map +1 -1
  32. package/dist/workers/incremental-mtime-tracker.d.ts +13 -0
  33. package/dist/workers/incremental-mtime-tracker.d.ts.map +1 -0
  34. package/dist/workers/incremental-mtime-tracker.js +65 -0
  35. package/dist/workers/incremental-mtime-tracker.js.map +1 -0
  36. package/dist/workers/library-build.worker.d.ts.map +1 -1
  37. package/dist/workers/library-build.worker.js +37 -15
  38. package/dist/workers/library-build.worker.js.map +1 -1
  39. package/dist/workers/server-build.worker.d.ts.map +1 -1
  40. package/dist/workers/server-build.worker.js +6 -5
  41. package/dist/workers/server-build.worker.js.map +1 -1
  42. package/dist/workers/server-esbuild-context.d.ts.map +1 -1
  43. package/dist/workers/server-esbuild-context.js +3 -1
  44. package/dist/workers/server-esbuild-context.js.map +1 -1
  45. package/dist/workers/server-watch-manager.js +1 -1
  46. package/dist/workers/server-watch-manager.js.map +1 -1
  47. package/package.json +5 -4
  48. package/src/angular/ngtsc-build-core.ts +73 -5
  49. package/src/commands/publish/version-upgrade.ts +43 -34
  50. package/src/deps/replace-deps/replace-deps-resolve.ts +12 -7
  51. package/src/deps/replace-deps/replace-deps.ts +90 -16
  52. package/src/deps/server-externals/server-production-files.ts +26 -28
  53. package/src/esbuild/esbuild-angular-compiler-plugin.ts +82 -123
  54. package/src/esbuild/esbuild-postcss-plugin.ts +9 -6
  55. package/src/esbuild/esbuild-worker-plugin.ts +266 -0
  56. package/src/workers/client.worker.ts +4 -23
  57. package/src/workers/incremental-mtime-tracker.ts +68 -0
  58. package/src/workers/library-build.worker.ts +41 -14
  59. package/src/workers/server-build.worker.ts +6 -5
  60. package/src/workers/server-esbuild-context.ts +3 -1
  61. package/src/workers/server-watch-manager.ts +1 -1
  62. package/tests/angular/ngtsc-build-core.acc.spec.ts +210 -0
  63. package/tests/angular/ngtsc-build-core.spec.ts +52 -0
  64. package/tests/commands/version-upgrade.acc.spec.ts +210 -0
  65. package/tests/commands/version-upgrade.spec.ts +148 -0
  66. package/tests/deps/replace-deps/replace-deps-perf.verify.md +15 -0
  67. package/tests/deps/replace-deps/replace-deps-resolve.acc.spec.ts +124 -0
  68. package/tests/esbuild/esbuild-angular-compiler-plugin-worker.verify.md +56 -28
  69. package/tests/esbuild/esbuild-postcss-plugin-chunking.verify.md +17 -0
  70. package/tests/esbuild/esbuild-postcss-plugin.acc.spec.ts +152 -0
  71. package/tests/esbuild/esbuild-worker-plugin-node.verify.md +11 -0
  72. package/tests/esbuild/esbuild-worker-plugin.acc.spec.ts +318 -0
  73. package/tests/esbuild/esbuild-worker-plugin.spec.ts +297 -0
  74. package/tests/esbuild/esbuild-worker-plugin.verify.md +7 -0
  75. package/tests/esbuild/fixtures/worker-plugin/node-worker.js +2 -0
  76. package/tests/esbuild/fixtures/worker-plugin/shared-worker.js +6 -0
  77. package/tests/esbuild/fixtures/worker-plugin/worker-error.js +1 -0
  78. package/tests/esbuild/fixtures/worker-plugin/worker.js +3 -0
  79. package/tests/esbuild/fixtures/worker-plugin/worker2.js +3 -0
  80. package/tests/utils/ngtsc-build-core-write-emit.spec.ts +124 -0
  81. package/tests/workers/client-worker-mtime-incremental.verify.md +10 -0
  82. package/tests/workers/incremental-mtime-tracker.acc.spec.ts +144 -0
  83. package/tests/workers/incremental-mtime-tracker.spec.ts +102 -0
  84. package/tests/workers/library-build-worker.spec.ts +4 -0
  85. package/tests/workers/server-build-worker-plugin.verify.md +9 -0
  86. package/tests/workers/server-build-worker.spec.ts +26 -12
  87. package/tests/workers/server-esbuild-context.spec.ts +13 -5
  88. package/tests/workers/server-watch-manager.acc.spec.ts +2 -2
  89. package/tests/workers/server-watch-manager.spec.ts +2 -2
  90. package/dist/angular/web-worker-transformer.d.ts +0 -9
  91. package/dist/angular/web-worker-transformer.d.ts.map +0 -1
  92. package/dist/angular/web-worker-transformer.js +0 -73
  93. package/dist/angular/web-worker-transformer.js.map +0 -1
  94. package/src/angular/web-worker-transformer.ts +0 -117
  95. package/tests/angular/web-worker-transformer.spec.ts +0 -154
  96. package/tests/ts-compiler/fixtures/non-angular-pkg/dist/index.d.ts +0 -2
  97. package/tests/ts-compiler/fixtures/non-angular-pkg/dist/index.d.ts.map +0 -1
  98. package/tests/ts-compiler/fixtures/non-angular-pkg/dist/index.js +0 -4
  99. package/tests/ts-compiler/fixtures/non-angular-pkg/dist/index.js.map +0 -1
  100. package/tests/ts-compiler/fixtures/non-angular-pkg/dist/util.d.ts +0 -2
  101. package/tests/ts-compiler/fixtures/non-angular-pkg/dist/util.d.ts.map +0 -1
  102. package/tests/ts-compiler/fixtures/non-angular-pkg/dist/util.js +0 -4
  103. package/tests/ts-compiler/fixtures/non-angular-pkg/dist/util.js.map +0 -1
@@ -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` 할당 후 수정 없음).
@@ -0,0 +1,124 @@
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 { pathx } from "@simplysm/core-node";
6
+ import { resolveAllReplaceDepEntries } from "../../../src/deps/replace-deps/replace-deps-resolve";
7
+ import { consola } from "consola";
8
+
9
+ describe("resolveAllReplaceDepEntries", () => {
10
+ let tmpDir: string;
11
+ let projectRoot: string;
12
+ const logger = consola.withTag("test");
13
+
14
+ beforeEach(async () => {
15
+ tmpDir = pathx.posix(await fs.promises.mkdtemp(path.join(os.tmpdir(), "sd-resolve-deps-")));
16
+ projectRoot = pathx.posix(path.join(tmpDir, "project"));
17
+ await fs.promises.mkdir(projectRoot, { recursive: true });
18
+ await fs.promises.writeFile(
19
+ pathx.posix(path.join(projectRoot, "pnpm-workspace.yaml")),
20
+ "packages:\n",
21
+ );
22
+ });
23
+
24
+ afterEach(async () => {
25
+ await fs.promises.rm(tmpDir, { recursive: true, force: true });
26
+ });
27
+
28
+ /**
29
+ * node_modules에 패키지 디렉토리를 생성하는 헬퍼
30
+ */
31
+ async function createNodeModulesPkg(
32
+ nodeModulesDir: string,
33
+ pkgName: string,
34
+ ): Promise<string> {
35
+ const pkgDir = pathx.posix(path.join(nodeModulesDir, pkgName));
36
+ await fs.promises.mkdir(pkgDir, { recursive: true });
37
+ await fs.promises.writeFile(
38
+ pathx.posix(path.join(pkgDir, "package.json")),
39
+ JSON.stringify({ name: pkgName }),
40
+ );
41
+ return pkgDir;
42
+ }
43
+
44
+ /**
45
+ * 소스 패키지를 생성하는 헬퍼
46
+ */
47
+ async function createSourcePkg(name: string): Promise<string> {
48
+ const sourcePath = pathx.posix(path.join(tmpDir, name));
49
+ await fs.promises.mkdir(sourcePath, { recursive: true });
50
+ await fs.promises.writeFile(
51
+ pathx.posix(path.join(sourcePath, "package.json")),
52
+ JSON.stringify({ name, files: ["dist"] }),
53
+ );
54
+ await fs.promises.mkdir(pathx.posix(path.join(sourcePath, "dist")), { recursive: true });
55
+ await fs.promises.writeFile(
56
+ pathx.posix(path.join(sourcePath, "dist", "index.js")),
57
+ "module.exports = {};",
58
+ );
59
+ return sourcePath;
60
+ }
61
+
62
+ it("동일 actualTargetPath를 가진 항목은 중복 등록되지 않는다", async () => {
63
+ // Given: 루트 node_modules와 workspace pkg의 node_modules에 동일 패키지가 존재하고
64
+ // 둘 다 같은 실제 경로(symlink 해석 후)를 가리킨다
65
+ const sourcePath = await createSourcePkg("@test/pkg");
66
+ const nodeModulesDir = pathx.posix(path.join(projectRoot, "node_modules"));
67
+ await createNodeModulesPkg(nodeModulesDir, "@test/pkg");
68
+
69
+ // workspace 패키지 설정 (pnpm-workspace.yaml에 추가)
70
+ const workspacePkgDir = pathx.posix(path.join(projectRoot, "packages", "my-app"));
71
+ await fs.promises.mkdir(workspacePkgDir, { recursive: true });
72
+ await fs.promises.writeFile(
73
+ pathx.posix(path.join(projectRoot, "pnpm-workspace.yaml")),
74
+ "packages:\n - packages/*\n",
75
+ );
76
+
77
+ // workspace 패키지의 node_modules에도 동일 패키지 생성
78
+ const wsPkgNodeModules = pathx.posix(path.join(workspacePkgDir, "node_modules"));
79
+ await createNodeModulesPkg(wsPkgNodeModules, "@test/pkg");
80
+
81
+ // When
82
+ const entries = await resolveAllReplaceDepEntries(
83
+ projectRoot,
84
+ { "@test/pkg": sourcePath },
85
+ logger,
86
+ );
87
+
88
+ // Then: actualTargetPath가 다르므로 2개가 반환되어야 한다
89
+ // (symlink가 아닌 실제 디렉토리이므로 actualTargetPath가 각각 다름)
90
+ // 중복 방지 로직은 동일 actualTargetPath일 때만 작동한다
91
+ const actualTargetPaths = entries.map((e) => e.actualTargetPath);
92
+ const uniqueActualTargetPaths = new Set(actualTargetPaths);
93
+ expect(actualTargetPaths.length).toBe(uniqueActualTargetPaths.size);
94
+ });
95
+
96
+ it("여러 replaceDeps 패턴이 올바르게 매칭되어 결과를 반환한다", async () => {
97
+ // Given: 2개의 다른 패턴과 각각의 소스 패키지
98
+ const sourceA = await createSourcePkg("@test/pkg-a");
99
+ const sourceB = await createSourcePkg("@other/lib");
100
+ const nodeModulesDir = pathx.posix(path.join(projectRoot, "node_modules"));
101
+ await createNodeModulesPkg(nodeModulesDir, "@test/pkg-a");
102
+ await createNodeModulesPkg(nodeModulesDir, "@other/lib");
103
+
104
+ // When: 2개 패턴으로 호출 (glob이 병렬 실행되어야 함)
105
+ const entries = await resolveAllReplaceDepEntries(
106
+ projectRoot,
107
+ {
108
+ "@test/pkg-a": sourceA,
109
+ "@other/lib": sourceB,
110
+ },
111
+ logger,
112
+ );
113
+
114
+ // Then: 두 패턴 모두 매칭되어 2개 entries 반환
115
+ expect(entries).toHaveLength(2);
116
+ const targetNames = entries.map((e) => e.targetName).sort();
117
+ expect(targetNames).toEqual(["@other/lib", "@test/pkg-a"]);
118
+ });
119
+
120
+ it("빈 replaceDeps 설정이면 빈 배열을 반환한다", async () => {
121
+ const entries = await resolveAllReplaceDepEntries(projectRoot, {}, logger);
122
+ expect(entries).toEqual([]);
123
+ });
124
+ });
@@ -1,31 +1,59 @@
1
- # Web Worker 번들링 — LLM 검증
1
+ # Web Worker 통합 (Feature 1.2) — LLM 검증
2
2
 
3
3
  ## 검증 항목
4
4
 
5
- ### bundleWebWorker 함수
6
-
7
- - [x] buildSync 옵션: `esbuild-angular-compiler-plugin.ts:165-177` — platform: "browser", write: false, bundle: true, metafile: true, format: "esm", entryNames: "worker-[hash]", plugins: undefined, supported: undefined 모두 확인
8
- - [x] sourcemap 전달: `esbuild-angular-compiler-plugin.ts:174` `sourcemap` 파라미터 직접 전달
9
- - [x] initialOptions spread: `esbuild-angular-compiler-plugin.ts:165` `...build.initialOptions`로 target/absWorkingDir/outdir 전달
10
- - [x] catch — errors/warnings 있으면 반환: `esbuild-angular-compiler-plugin.ts:178-186` `"errors" in error && "warnings" in error` 확인 후 `error as esbuild.BuildResult` 반환
11
- - [x] catch — 없으면 re-throw: `esbuild-angular-compiler-plugin.ts:187` `throw error`
12
-
13
- ### processWebWorker 콜백
14
-
15
- - [x] workerFile resolve: `esbuild-angular-compiler-plugin.ts:346` — `path.join(path.dirname(containingFile), workerFile)`
16
- - [x] 성공 — additionalResults 저장: `esbuild-angular-compiler-plugin.ts:365-368` — `additionalResults.set(fullWorkerPath, { outputFiles, metafile })`
17
- - [x] 성공 — FileReferenceTracker 등록: `esbuild-angular-compiler-plugin.ts:371-376` — `metafile.inputs` 키를 `path.join(cwd, input)`으로 변환하여 등록
18
- - [x] 성공 반환값: `esbuild-angular-compiler-plugin.ts:379-388` `/^worker-[A-Z0-9]{8}\.[cm]?js$/` 패턴으로 검색, `path.relative(outdir, ...)` + `replaceAll("\\", "/")` forward slash 반환
19
- - [x] 에러 errors push: `esbuild-angular-compiler-plugin.ts:352` `errors.push(...workerResult.errors)`
20
- - [x] 에러 — warnings push: `esbuild-angular-compiler-plugin.ts:349` — `warnings.push(...workerResult.warnings)` (성공/실패 공통 실행)
21
- - [x] 에러 FileReferenceTracker 등록: `esbuild-angular-compiler-plugin.ts:354-359` `location?.file` 필터 후 `path.join(cwd, f)` 변환
22
- - [x] 에러 — additionalResults 저장: `esbuild-angular-compiler-plugin.ts:361` — `{ errors: workerResult.errors }`
23
- - [x] 에러 원본 반환: `esbuild-angular-compiler-plugin.ts:362` `return workerFile`
24
-
25
- ### Plugin 통합
26
-
27
- - [x] import: `esbuild-angular-compiler-plugin.ts:14` `import { createWorkerTransformer } from "../angular/web-worker-transformer.js"`
28
- - [x] transformer 생성: `esbuild-angular-compiler-plugin.ts:392` `createWorkerTransformer(processWebWorker)`
29
- - [x] emitAffectedFiles 전달: `esbuild-angular-compiler-plugin.ts:393-395` `{ additionalTransformers: { before: [workerTransformer] } }`
30
- - [x] 클로저 접근: `esbuild-angular-compiler-plugin.ts:344-389` onStart 내부에서 생성, `errors`/`warnings`/`additionalResults`/`referencedFileTracker`/`cwd`/`build` 접근 가능
31
- - [x] onEnd 병합: `esbuild-angular-compiler-plugin.ts:449-460` — additionalResults 순회에서 worker 결과 포함
5
+ ### 제거된 코드
6
+
7
+ - [x] `createWorkerTransformer` import가 제거됨: `esbuild-angular-compiler-plugin.ts`에 `web-worker-transformer.js`로부터의 import가 존재하지 않음
8
+ - [x] `AdditionalResult` 인터페이스 선언이 제거됨
9
+ - [x] `bundleWebWorker` 함수 `#region bundleWebWorker` 주석이 제거됨
10
+ - [x] `additionalResults` Map 선언이 제거됨
11
+ - [x] `createWebWorkerProcessor` 함수가 제거됨
12
+ - [x] onStart 내부의 `processWebWorker` + `workerTransformer` 생성 코드가 제거됨
13
+ - [x] `compileAsync` 호출에서 `additionalTransformers` 옵션이 제거됨
14
+ - [x] 증분 빌드 루프에서 `additionalResults.delete(file)` 호출이 제거됨
15
+
16
+ ### 추가된 코드
17
+
18
+ - [x] `transformWorkerPatterns` import가 `./esbuild-worker-plugin`에서 추가됨 (같은 디렉토리, .js 확장자 미사용)
19
+ - [x] setup 스코프에 `workerResultsByContainingFile = new Map<string, { outputFiles?: esbuild.OutputFile[]; metafile?: esbuild.Metafile }>()` 선언이 존재함 (증분 빌드 시 변경되지 않은 Worker metafile 유지 목적)
20
+
21
+ ### onStartTS 파일 Worker 패턴 처리 (Rule 1)
22
+
23
+ - [x] 증분 빌드 `expandedModifiedFiles` 각 파일에 대해 `workerResultsByContainingFile.delete(file)` 호출 (선택적 제거)
24
+ - [x] `emitResults` 루프에서 각 파일에 대해 `transformWorkerPatterns(contents, normalized, build)` 호출
25
+ - [x] `workerResult != null`이면 `typeScriptFileCache.set(normalized, workerResult.contents)` 저장
26
+ - [x] `workerResult.errors`/`workerResult.warnings`를 onStart의 `errors`/`warnings` 배열에 push
27
+ - [x] `workerResult.workerMetafile != null`이면 `referencedFileTracker.add(normalized, Object.keys(workerResult.workerMetafile.inputs).map(input => path.join(cwd, input)))` 호출
28
+ - [x] `workerMetafile` 또는 `workerOutputFiles`가 있으면 `workerResultsByContainingFile.set(normalized, { outputFiles, metafile })` 저장
29
+ - [x] `workerResult == null`이면 기존처럼 `typeScriptFileCache.set(normalized, contents)` 저장
30
+ - [x] Worker 번들 에러가 errors 배열에 포함되어 onStart 결과에 반영됨 (Scenario: 에러 전파)
31
+
32
+ ### JS onLoad — .js 파일 Worker 패턴 처리 (Rule 2)
33
+
34
+ - [x] `createCachedLoad` 콜백에서 `javascriptTransformer.transformFile` 호출 후 결과를 `TextDecoder`로 문자열로 변환
35
+ - [x] 변환된 문자열에 `transformWorkerPatterns(textContents, request, build)` 적용
36
+ - [x] `workerResult != null`일 때:
37
+ - `workerResult.workerMetafile`이 있으면 `referencedFileTracker.add` 호출
38
+ - `workerMetafile` 또는 `workerOutputFiles`가 있으면 `workerResultsByContainingFile.set(request, { outputFiles, metafile })` 저장
39
+ - 반환: `{ contents, loader: "js", resolveDir, errors (>0일때), warnings (>0일때) }` — TS/JS 에러 처리 일관성 확보 (L2 리뷰 반영)
40
+ - [x] `workerResult == null`일 때 기존과 동일한 `{ contents, loader: "js", resolveDir }` 반환
41
+
42
+ ### onEnd — metafile 병합 (Rule 3)
43
+
44
+ - [x] onEnd에서 `workerResultsByContainingFile.values()` 순회
45
+ - [x] 각 항목의 `outputFiles`가 있으면 `result.outputFiles?.push(...outputFiles)`
46
+ - [x] 각 항목의 `metafile`이 있으면 `Object.assign(result.metafile.inputs, wr.metafile.inputs)` + `Object.assign(result.metafile.outputs, wr.metafile.outputs)`
47
+ - [x] onEnd에서 Map 전체 리셋 없음 — 증분 빌드에서 변경되지 않은 Worker 결과가 다음 빌드에서도 병합됨
48
+ - [x] 기존 `additionalResults` 순회 루프가 제거됨
49
+
50
+ ### client config 무변경 (Rule 6)
51
+
52
+ - [x] `esbuild-client-config.ts`에 Worker 플러그인 추가 코드가 존재하지 않음 (plugins 배열에 `createWorkerBundlePlugin` 또는 `sd-worker-bundle` 미포함)
53
+ - [x] `esbuild-client-config.ts` 파일 자체가 Feature 1.2로 인해 변경되지 않음 (git diff 기준)
54
+
55
+ ### 회귀 방지
56
+
57
+ - [x] `pnpm test --run packages/sd-cli/tests/esbuild/esbuild-angular-compiler-plugin.spec.ts` 통과 (기존 plugin 구조 테스트)
58
+ - [x] `pnpm test --run packages/sd-cli/tests/esbuild/esbuild-worker-plugin.spec.ts` 통과 (Feature 1.1 Unit 테스트)
59
+ - [x] `pnpm test --run packages/sd-cli/tests/esbuild/esbuild-worker-plugin.acc.spec.ts` 통과 (Feature 1.1 Acceptance 테스트)
@@ -0,0 +1,17 @@
1
+ # PostCSS 문자열 교체 청크화 — LLM 검증
2
+
3
+ ## 검증 항목
4
+
5
+ - [x] 정방향 청크 배열 + join이 역순 slice와 수학적으로 동일한 결과를 생성하는가:
6
+ 역순 slice 방식은 `code[0..start_n] + rep_n + code[end_n..start_{n-1}] + rep_{n-1} + ... + code[end_1..]` 순서로 최종 문자열을 구성한다.
7
+ 정방향 청크 방식은 `code[0..start_1] + rep_1 + code[end_1..start_2] + rep_2 + ... + code[end_n..]` 순서로 청크를 수집하고 join한다.
8
+ 두 방식 모두 원본 텍스트의 비교체 구간과 PostCSS 처리된 교체 텍스트를 동일한 순서로 결합하므로, replacements가 비중첩(acorn AST 노드 보장)인 한 결과가 동일하다.
9
+ 검증 결과: **동일성 보장됨**
10
+
11
+ - [x] cursor 초기값 0과 마지막 `code.slice(cursor)` 호출이 파일 전체를 커버하는가:
12
+ cursor는 0에서 시작하여 각 replacement의 end로 갱신된다. 마지막 `chunks.push(code.slice(cursor))`가 마지막 replacement 이후의 원본 텍스트를 수집한다. replacements가 0건이면 `code.slice(0)` = 전체 코드가 되지만, 이 경우 `replacements.length === 0`에서 continue되므로 도달하지 않는다.
13
+ 검증 결과: **전체 커버됨**
14
+
15
+ - [x] `replacements.sort()`가 원본 배열을 변경하는 것이 문제되지 않는가:
16
+ `replacements`는 각 파일 처리 시 새로 생성되는 로컬 배열이므로 in-place sort가 안전하다. 기존 코드도 동일하게 in-place sort를 사용했다.
17
+ 검증 결과: **문제 없음**