@simplysm/sd-cli 14.0.19 → 14.0.22

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 (114) hide show
  1. package/dist/angular/vite-postcss-inline-plugin.d.ts.map +1 -1
  2. package/dist/angular/vite-postcss-inline-plugin.js +4 -1
  3. package/dist/angular/vite-postcss-inline-plugin.js.map +1 -1
  4. package/dist/capacitor/capacitor-android.d.ts +16 -0
  5. package/dist/capacitor/capacitor-android.d.ts.map +1 -0
  6. package/dist/capacitor/capacitor-android.js +289 -0
  7. package/dist/capacitor/capacitor-android.js.map +1 -0
  8. package/dist/capacitor/capacitor.d.ts +0 -49
  9. package/dist/capacitor/capacitor.d.ts.map +1 -1
  10. package/dist/capacitor/capacitor.js +4 -244
  11. package/dist/capacitor/capacitor.js.map +1 -1
  12. package/dist/commands/check.js +2 -2
  13. package/dist/commands/check.js.map +1 -1
  14. package/dist/commands/lint.d.ts +1 -42
  15. package/dist/commands/lint.d.ts.map +1 -1
  16. package/dist/commands/lint.js +1 -151
  17. package/dist/commands/lint.js.map +1 -1
  18. package/dist/commands/publish.d.ts.map +1 -1
  19. package/dist/commands/publish.js +2 -1
  20. package/dist/commands/publish.js.map +1 -1
  21. package/dist/commands/typecheck.d.ts +3 -40
  22. package/dist/commands/typecheck.d.ts.map +1 -1
  23. package/dist/commands/typecheck.js +3 -232
  24. package/dist/commands/typecheck.js.map +1 -1
  25. package/dist/electron/electron.js +11 -4
  26. package/dist/electron/electron.js.map +1 -1
  27. package/dist/engines/ViteEngine.js +1 -1
  28. package/dist/engines/ViteEngine.js.map +1 -1
  29. package/dist/engines/types.d.ts +2 -0
  30. package/dist/engines/types.d.ts.map +1 -1
  31. package/dist/infra/ResultCollector.d.ts +2 -2
  32. package/dist/infra/ResultCollector.d.ts.map +1 -1
  33. package/dist/infra/ResultCollector.js +1 -1
  34. package/dist/orchestrators/DevWatchOrchestrator.d.ts +2 -1
  35. package/dist/orchestrators/DevWatchOrchestrator.d.ts.map +1 -1
  36. package/dist/orchestrators/DevWatchOrchestrator.js +28 -16
  37. package/dist/orchestrators/DevWatchOrchestrator.js.map +1 -1
  38. package/dist/orchestrators/TypecheckOrchestrator.d.ts +74 -0
  39. package/dist/orchestrators/TypecheckOrchestrator.d.ts.map +1 -0
  40. package/dist/orchestrators/TypecheckOrchestrator.js +285 -0
  41. package/dist/orchestrators/TypecheckOrchestrator.js.map +1 -0
  42. package/dist/sd-cli.js +6 -1
  43. package/dist/sd-cli.js.map +1 -1
  44. package/dist/utils/lint-core.d.ts +43 -0
  45. package/dist/utils/lint-core.d.ts.map +1 -0
  46. package/dist/utils/lint-core.js +154 -0
  47. package/dist/utils/lint-core.js.map +1 -0
  48. package/dist/utils/lint-utils.d.ts +1 -1
  49. package/dist/utils/lint-utils.d.ts.map +1 -1
  50. package/dist/utils/output-utils.d.ts +2 -2
  51. package/dist/utils/output-utils.d.ts.map +1 -1
  52. package/dist/utils/output-utils.js.map +1 -1
  53. package/dist/utils/server-production-files.d.ts +22 -0
  54. package/dist/utils/server-production-files.d.ts.map +1 -0
  55. package/dist/utils/server-production-files.js +162 -0
  56. package/dist/utils/server-production-files.js.map +1 -0
  57. package/dist/utils/vite-config.js.map +1 -1
  58. package/dist/workers/client.worker.d.ts.map +1 -1
  59. package/dist/workers/client.worker.js +7 -1
  60. package/dist/workers/client.worker.js.map +1 -1
  61. package/dist/workers/lint.worker.d.ts +1 -1
  62. package/dist/workers/lint.worker.d.ts.map +1 -1
  63. package/dist/workers/lint.worker.js +1 -1
  64. package/dist/workers/lint.worker.js.map +1 -1
  65. package/dist/workers/server-build.worker.d.ts.map +1 -1
  66. package/dist/workers/server-build.worker.js +12 -161
  67. package/dist/workers/server-build.worker.js.map +1 -1
  68. package/package.json +4 -4
  69. package/src/angular/vite-postcss-inline-plugin.ts +5 -1
  70. package/src/capacitor/capacitor-android.ts +368 -0
  71. package/src/capacitor/capacitor.ts +4 -317
  72. package/src/commands/check.ts +2 -2
  73. package/src/commands/lint.ts +1 -201
  74. package/src/commands/publish.ts +2 -1
  75. package/src/commands/typecheck.ts +7 -292
  76. package/src/electron/electron.ts +4 -4
  77. package/src/engines/ViteEngine.ts +1 -1
  78. package/src/engines/types.ts +3 -0
  79. package/src/infra/ResultCollector.ts +2 -2
  80. package/src/orchestrators/DevWatchOrchestrator.ts +35 -20
  81. package/src/orchestrators/TypecheckOrchestrator.ts +364 -0
  82. package/src/sd-cli.ts +6 -1
  83. package/src/utils/lint-core.ts +205 -0
  84. package/src/utils/lint-utils.ts +1 -1
  85. package/src/utils/output-utils.ts +3 -3
  86. package/src/utils/server-production-files.ts +186 -0
  87. package/src/utils/vite-config.ts +1 -1
  88. package/src/workers/client.worker.ts +7 -1
  89. package/src/workers/lint.worker.ts +1 -1
  90. package/src/workers/server-build.worker.ts +11 -185
  91. package/tests/angular/vite-postcss-inline-plugin.spec.ts +10 -0
  92. package/tests/capacitor/capacitor-android-exports.verify.md +11 -0
  93. package/tests/capacitor/capacitor-android.spec.ts +219 -0
  94. package/tests/capacitor/capacitor-build.spec.ts +17 -21
  95. package/tests/capacitor/capacitor-icon.spec.ts +17 -19
  96. package/tests/capacitor/capacitor-init.spec.ts +18 -14
  97. package/tests/capacitor/capacitor-run.spec.ts +10 -24
  98. package/tests/capacitor/capacitor-workspace.spec.ts +10 -15
  99. package/tests/commands/check.spec.ts +2 -2
  100. package/tests/commands/lint.spec.ts +33 -194
  101. package/tests/commands/publish-set.verify.md +7 -0
  102. package/tests/electron/electron-symlink-cleanup.verify.md +8 -0
  103. package/tests/engines/vite-engine.spec.ts +29 -0
  104. package/tests/infra/result-collector.spec.ts +11 -0
  105. package/tests/orchestrators/dev-watch-orchestrator.spec.ts +70 -0
  106. package/tests/orchestrators/dist-delete-watcher.verify.md +10 -0
  107. package/tests/orchestrators/typecheck-orchestrator.spec.ts +180 -0
  108. package/tests/sd-cli-catch-all.verify.md +7 -0
  109. package/tests/utils/lint-core-import-paths.verify.md +10 -0
  110. package/tests/utils/lint-core.spec.ts +188 -0
  111. package/tests/utils/server-production-files-import-paths.verify.md +14 -0
  112. package/tests/workers/client-worker.spec.ts +92 -0
  113. package/tests/workers/server-build-context-dispose.verify.md +8 -0
  114. package/tests/workers/server-build-worker.spec.ts +39 -0
@@ -0,0 +1,188 @@
1
+ import { describe, it, expect, vi, beforeEach } from "vitest";
2
+
3
+ // Hoisted mock references — available inside vi.mock factories
4
+ const mocks = vi.hoisted(() => ({
5
+ fsxExists: vi.fn<(path: string) => Promise<boolean>>(),
6
+ fsxGlob: vi.fn<(...args: unknown[]) => Promise<string[]>>(),
7
+ filterByTargets: vi.fn(
8
+ (files: string[], _targets: string[], _cwd: string) => files,
9
+ ),
10
+ lintFiles: vi.fn<() => Promise<Array<{ errorCount: number; warningCount: number }>>>(),
11
+ loadFormatter: vi.fn(),
12
+ outputFixes: vi.fn(),
13
+ jitiImport: vi.fn(),
14
+ eslintCtor: vi.fn(),
15
+ }));
16
+
17
+ vi.mock("@simplysm/core-node", () => ({
18
+ fsx: { exists: mocks.fsxExists, glob: mocks.fsxGlob },
19
+ pathx: { filterByTargets: mocks.filterByTargets },
20
+ }));
21
+
22
+ vi.mock("eslint", () => ({
23
+ ESLint: class MockESLint {
24
+ constructor(options: unknown) { mocks.eslintCtor(options); }
25
+ lintFiles = mocks.lintFiles;
26
+ loadFormatter = mocks.loadFormatter;
27
+ static outputFixes = mocks.outputFixes;
28
+ },
29
+ }));
30
+
31
+ vi.mock("jiti", () => ({
32
+ createJiti: vi.fn(() => ({ import: mocks.jitiImport })),
33
+ }));
34
+
35
+ vi.mock("consola", () => {
36
+ const fns = (): Record<string, unknown> => ({
37
+ debug: vi.fn(), start: vi.fn(), success: vi.fn(),
38
+ info: vi.fn(), error: vi.fn(), warn: vi.fn(), log: vi.fn(),
39
+ withTag: vi.fn(() => fns()),
40
+ level: 0,
41
+ });
42
+ const c = fns();
43
+ return { consola: c, default: c, LogLevels: {} };
44
+ });
45
+
46
+ const { loadIgnorePatterns, executeLint } = await import("../../src/utils/lint-core");
47
+
48
+ //#region loadIgnorePatterns
49
+
50
+ describe("loadIgnorePatterns", () => {
51
+ beforeEach(() => vi.clearAllMocks());
52
+
53
+ it("extracts globalIgnores patterns from eslint config", async () => {
54
+ mocks.fsxExists.mockImplementation((p) =>
55
+ Promise.resolve(typeof p === "string" && p.endsWith("eslint.config.ts")),
56
+ );
57
+ mocks.jitiImport.mockResolvedValue({
58
+ default: [
59
+ { ignores: ["dist/**", "node_modules/**"] },
60
+ { files: ["*.ts"], rules: {} }, // not globalIgnores — has files key
61
+ { ignores: [".cache/**"] },
62
+ ],
63
+ });
64
+
65
+ const result = await loadIgnorePatterns("/project");
66
+ expect(result).toEqual(["dist/**", "node_modules/**", ".cache/**"]);
67
+ });
68
+
69
+ it("ignores config objects that have files key", async () => {
70
+ mocks.fsxExists.mockImplementation((p) =>
71
+ Promise.resolve(typeof p === "string" && p.endsWith("eslint.config.ts")),
72
+ );
73
+ mocks.jitiImport.mockResolvedValue({
74
+ default: [{ files: ["*.ts"], ignores: ["dist/**"] }],
75
+ });
76
+
77
+ const result = await loadIgnorePatterns("/project");
78
+ expect(result).toEqual([]);
79
+ });
80
+
81
+ it("throws when no eslint config file found", async () => {
82
+ mocks.fsxExists.mockResolvedValue(false);
83
+
84
+ await expect(loadIgnorePatterns("/project")).rejects.toThrow(
85
+ "ESLint 설정 파일",
86
+ );
87
+ });
88
+ });
89
+
90
+ //#endregion
91
+
92
+ //#region executeLint
93
+
94
+ describe("executeLint", () => {
95
+ beforeEach(() => {
96
+ vi.clearAllMocks();
97
+ // Default: eslint config exists with no ignores
98
+ mocks.fsxExists.mockImplementation((p) =>
99
+ Promise.resolve(typeof p === "string" && p.endsWith("eslint.config.ts")),
100
+ );
101
+ mocks.jitiImport.mockResolvedValue({ default: [] });
102
+ mocks.fsxGlob.mockResolvedValue(["/project/src/a.ts", "/project/src/b.ts"]);
103
+ mocks.filterByTargets.mockImplementation((files) => files);
104
+ mocks.lintFiles.mockResolvedValue([{ errorCount: 0, warningCount: 0 }]);
105
+ mocks.loadFormatter.mockResolvedValue({
106
+ format: vi.fn().mockResolvedValue(""),
107
+ });
108
+ });
109
+
110
+ it("lints all collected files and returns success when no errors", async () => {
111
+ const result = await executeLint({ targets: [], fix: false, timing: false });
112
+
113
+ expect(result.success).toBe(true);
114
+ expect(result.errorCount).toBe(0);
115
+ expect(result.warningCount).toBe(0);
116
+ expect(mocks.lintFiles).toHaveBeenCalled();
117
+ });
118
+
119
+ it("filters files by targets via pathx.filterByTargets", async () => {
120
+ const filteredFiles = ["/project/packages/core-common/src/a.ts"];
121
+ mocks.filterByTargets.mockReturnValue(filteredFiles);
122
+
123
+ await executeLint({ targets: ["packages/core-common"], fix: false, timing: false });
124
+
125
+ expect(mocks.filterByTargets).toHaveBeenCalledWith(
126
+ expect.any(Array),
127
+ ["packages/core-common"],
128
+ expect.any(String),
129
+ );
130
+ });
131
+
132
+ it("applies auto-fix when fix option is true", async () => {
133
+ await executeLint({ targets: [], fix: true, timing: false });
134
+
135
+ expect(mocks.outputFixes).toHaveBeenCalled();
136
+ });
137
+
138
+ it("sets TIMING env variable when timing option is true", async () => {
139
+ const origTiming = process.env["TIMING"];
140
+
141
+ await executeLint({ targets: [], fix: false, timing: true });
142
+
143
+ expect(process.env["TIMING"]).toBe("1");
144
+
145
+ // Cleanup
146
+ if (origTiming == null) delete process.env["TIMING"];
147
+ else process.env["TIMING"] = origTiming;
148
+ });
149
+
150
+ it("creates ESLint with cache enabled and correct cache location", async () => {
151
+ await executeLint({ targets: [], fix: false, timing: false });
152
+
153
+ expect(mocks.eslintCtor).toHaveBeenCalledWith(
154
+ expect.objectContaining({
155
+ cache: true,
156
+ cacheLocation: expect.stringContaining("eslint.cache"),
157
+ }),
158
+ );
159
+ });
160
+
161
+ it("returns error count and formatted output when lint errors found", async () => {
162
+ mocks.lintFiles.mockResolvedValue([
163
+ { errorCount: 3, warningCount: 1 },
164
+ { errorCount: 1, warningCount: 2 },
165
+ ]);
166
+ mocks.loadFormatter.mockResolvedValue({
167
+ format: vi.fn().mockResolvedValue("error details"),
168
+ });
169
+
170
+ const result = await executeLint({ targets: [], fix: false, timing: false });
171
+
172
+ expect(result.success).toBe(false);
173
+ expect(result.errorCount).toBe(4);
174
+ expect(result.warningCount).toBe(3);
175
+ expect(result.formattedOutput).toBe("error details");
176
+ });
177
+
178
+ it("returns success when no files to lint", async () => {
179
+ mocks.fsxGlob.mockResolvedValue([]);
180
+
181
+ const result = await executeLint({ targets: [], fix: false, timing: false });
182
+
183
+ expect(result.success).toBe(true);
184
+ expect(mocks.lintFiles).not.toHaveBeenCalled();
185
+ });
186
+ });
187
+
188
+ //#endregion
@@ -0,0 +1,14 @@
1
+ # server-build.worker 비즈니스 로직 분리 — LLM 검증
2
+
3
+ ## 검증 항목
4
+
5
+ - [x] 4개 함수가 utils/server-production-files.ts에 export되어 있다: line 14, 27, 68, 87에 각각 export function 확인
6
+ - [x] worker에서 4개 함수 정의가 제거되었다: worker에 함수 정의 없음, import와 호출만 존재
7
+ - [x] worker에서 collectAllExternals, generateProductionFiles를 새 모듈에서 import한다: line 19 `import { collectAllExternals, generateProductionFiles } from "../utils/server-production-files"`
8
+ - [x] collectAllExternals 시그니처: `(pkgDir: string, manualExternals?: string[]) => string[]` — line 14 확인
9
+ - [x] parseLockfileVersions 시그니처: `(cwd: string) => Map<string, string>` — line 27 확인
10
+ - [x] resolveLockedVersions 시그니처: `(cwd: string, pkgNames: string[]) => Record<string, string>` — line 68 확인
11
+ - [x] generateProductionFiles 시그니처: `(info: ServerBuildInfo, externals: string[]) => void` — line 87 확인
12
+ - [x] worker에 build, rebuildAll, startWatch, stopWatch, cleanup, createEsbuildWatchContext가 존재한다: line 116, 148, 259, 336, 359, 488 확인
13
+ - [x] worker에서 cpx import가 제거되었다: grep 결과 매칭 없음
14
+ - [x] worker에서 collectAllDependencyExternals import가 제거되었다: grep 결과 매칭 없음
@@ -737,4 +737,96 @@ describe("client.worker", () => {
737
737
  await workerFns["stopWatch"]();
738
738
  });
739
739
  });
740
+
741
+ describe("build", () => {
742
+ // Acceptance: Scenario "Angular 컴파일 에러 시 빌드 실패 반환"
743
+ it("returns failure with errors when Angular compilation fails", async () => {
744
+ let capturedOnBuild: ((result: any) => void) | undefined;
745
+ mockCreateClientViteConfig.mockImplementation((opts: any) => {
746
+ capturedOnBuild = opts.onBuild;
747
+ return { plugins: [] };
748
+ });
749
+
750
+ mockViteBuild.mockImplementation(() => {
751
+ capturedOnBuild?.({
752
+ success: false,
753
+ errors: ["TS2345: type mismatch"],
754
+ warnings: [],
755
+ });
756
+ });
757
+
758
+ const result = await workerFns["build"](createBaseInfo());
759
+
760
+ expect(result.success).toBe(false);
761
+ expect(result.errors).toContain("TS2345: type mismatch");
762
+ });
763
+
764
+ // Acceptance: Scenario "Angular 컴파일 에러 없이 빌드 성공"
765
+ it("returns success when Angular compilation has no errors", async () => {
766
+ let capturedOnBuild: ((result: any) => void) | undefined;
767
+ mockCreateClientViteConfig.mockImplementation((opts: any) => {
768
+ capturedOnBuild = opts.onBuild;
769
+ return { plugins: [] };
770
+ });
771
+
772
+ mockViteBuild.mockImplementation(() => {
773
+ capturedOnBuild?.({ success: true });
774
+ });
775
+
776
+ const result = await workerFns["build"](createBaseInfo());
777
+
778
+ expect(result.success).toBe(true);
779
+ expect(result.errors).toBeUndefined();
780
+ });
781
+
782
+ // Acceptance: Scenario "Angular 컴파일 경고만 있을 때 성공 반환"
783
+ it("returns success with warnings when compilation has warnings only", async () => {
784
+ let capturedOnBuild: ((result: any) => void) | undefined;
785
+ mockCreateClientViteConfig.mockImplementation((opts: any) => {
786
+ capturedOnBuild = opts.onBuild;
787
+ return { plugins: [] };
788
+ });
789
+
790
+ mockViteBuild.mockImplementation(() => {
791
+ capturedOnBuild?.({
792
+ success: true,
793
+ warnings: ["deprecated API usage"],
794
+ });
795
+ });
796
+
797
+ const result = await workerFns["build"](createBaseInfo());
798
+
799
+ expect(result.success).toBe(true);
800
+ expect(result.warnings).toContain("deprecated API usage");
801
+ });
802
+
803
+ // Unit: lint result is still captured alongside compilation result
804
+ it("captures lint result from onBuild callback", async () => {
805
+ let capturedOnBuild: ((result: any) => void) | undefined;
806
+ mockCreateClientViteConfig.mockImplementation((opts: any) => {
807
+ capturedOnBuild = opts.onBuild;
808
+ return { plugins: [] };
809
+ });
810
+
811
+ const lintData = { success: true, errorCount: 0, warningCount: 0, formattedOutput: "" };
812
+ mockViteBuild.mockImplementation(() => {
813
+ capturedOnBuild?.({ success: true, lint: lintData });
814
+ });
815
+
816
+ const result = await workerFns["build"](createBaseInfo());
817
+
818
+ expect(result.success).toBe(true);
819
+ expect(result.lint).toEqual(lintData);
820
+ });
821
+
822
+ // Unit: returns success true when onBuild is never called (non-Angular framework)
823
+ it("returns success true when onBuild is never called", async () => {
824
+ mockCreateClientViteConfig.mockResolvedValue({ plugins: [] });
825
+ mockViteBuild.mockResolvedValue(undefined);
826
+
827
+ const result = await workerFns["build"](createBaseInfo());
828
+
829
+ expect(result.success).toBe(true);
830
+ });
831
+ });
740
832
  });
@@ -0,0 +1,8 @@
1
+ # esbuild context dispose 안전성 -- LLM 검증
2
+
3
+ ## 검증 항목
4
+
5
+ - [x] try-finally 패턴 적용: `server-build.worker.ts:434-442` — oldContext 캡처 후 try 블록에서 새 context 생성, finally에서 oldContext.dispose() 호출
6
+ - [x] 성공 시 dispose: try 블록에서 새 context 생성 성공 → finally에서 oldContext.dispose() 실행
7
+ - [x] 실패 시 dispose: createEsbuildWatchContext() 예외 시 → finally에서 oldContext.dispose() 실행 보장
8
+ - [x] null 안전: `if (oldContext != null)` 가드로 oldContext가 없는 경우(output.js=false 등) 안전
@@ -716,6 +716,45 @@ describe("server-build.worker startWatch()", () => {
716
716
  await workerFns["startWatch"](watchInfo);
717
717
  await expect(workerFns["startWatch"](watchInfo)).rejects.toThrow("already been called");
718
718
  });
719
+
720
+ // Acceptance: esbuild context creation failure leaves safe state (LOGIC-001)
721
+ it("allows tsc-only rebuilds after esbuild context recreation failure", async () => {
722
+ // Provide metafile inputs so subsequent change passes the metafile filter
723
+ mockMetafileInputs = { "packages/my-server/src/main.ts": {} };
724
+
725
+ await workerFns["startWatch"](watchInfo);
726
+
727
+ const onChangeHandler = mockOnChange.mock.calls[0][1] as (
728
+ changes: Array<{ event: string; path: string }>,
729
+ ) => Promise<void>;
730
+
731
+ // After dispose, rebuild should throw (simulates real disposed context)
732
+ mockDispose.mockImplementation(() => {
733
+ mockRebuild.mockRejectedValue(new Error("Build context already disposed"));
734
+ });
735
+
736
+ // Make context() throw to simulate creation failure
737
+ vi.mocked(esbuild.context).mockRejectedValueOnce(new Error("context creation failed"));
738
+ mockSend.mockClear();
739
+
740
+ // File add triggers context recreation → fails → sends "error"
741
+ await onChangeHandler([{ event: "add", path: "/workspace/packages/my-server/src/new.ts" }]);
742
+ expect(mockDispose).toHaveBeenCalled();
743
+
744
+ // Subsequent file change should work without "disposed" errors
745
+ mockSend.mockClear();
746
+ const absPath = path.resolve("/workspace", "packages/my-server/src/main.ts").replace(/\\/g, "/");
747
+ await onChangeHandler([{ event: "change", path: absPath }]);
748
+
749
+ // Should NOT get "disposed" error
750
+ const errorCalls = mockSend.mock.calls.filter((c) => c[0] === "error");
751
+ for (const [, data] of errorCalls) {
752
+ expect((data as { message: string }).message).not.toContain("disposed");
753
+ }
754
+ // Build event should be sent (tsc-only result)
755
+ const buildCalls = mockSend.mock.calls.filter((c) => c[0] === "build");
756
+ expect(buildCalls.length).toBeGreaterThanOrEqual(1);
757
+ });
719
758
  });
720
759
 
721
760
  describe("server-build.worker stopWatch()", () => {