@simplysm/sd-cli 14.1.11 → 14.1.13

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 (72) hide show
  1. package/dist/capacitor/capacitor-icon.js +2 -2
  2. package/dist/capacitor/capacitor-icon.js.map +1 -1
  3. package/dist/capacitor/capacitor-npm-config.d.ts +1 -1
  4. package/dist/capacitor/capacitor-npm-config.d.ts.map +1 -1
  5. package/dist/capacitor/capacitor-npm-config.js +11 -6
  6. package/dist/capacitor/capacitor-npm-config.js.map +1 -1
  7. package/dist/capacitor/capacitor.d.ts +1 -1
  8. package/dist/capacitor/capacitor.js +2 -2
  9. package/dist/capacitor/capacitor.js.map +1 -1
  10. package/dist/commands/device.js +2 -2
  11. package/dist/commands/device.js.map +1 -1
  12. package/dist/commands/init/generators/root.d.ts.map +1 -1
  13. package/dist/commands/init/generators/root.js +1 -0
  14. package/dist/commands/init/generators/root.js.map +1 -1
  15. package/dist/commands/init/init-client.js +1 -1
  16. package/dist/commands/init/init-client.js.map +1 -1
  17. package/dist/commands/init/init.js +2 -2
  18. package/dist/commands/init/init.js.map +1 -1
  19. package/dist/commands/publish/npm-publisher.js +3 -3
  20. package/dist/commands/publish/npm-publisher.js.map +1 -1
  21. package/dist/commands/publish/publish-command.js +1 -1
  22. package/dist/commands/publish/publish-command.js.map +1 -1
  23. package/dist/commands/publish/version-upgrade.d.ts.map +1 -1
  24. package/dist/commands/publish/version-upgrade.js +0 -50
  25. package/dist/commands/publish/version-upgrade.js.map +1 -1
  26. package/dist/deps/replace-deps/collect-deps.js +1 -1
  27. package/dist/deps/replace-deps/replace-deps-resolve.d.ts +2 -2
  28. package/dist/deps/replace-deps/replace-deps-resolve.js +2 -2
  29. package/dist/deps/replace-deps/replace-deps.d.ts +2 -2
  30. package/dist/deps/replace-deps/replace-deps.js +2 -2
  31. package/dist/deps/server-externals/server-production-files.d.ts +3 -3
  32. package/dist/deps/server-externals/server-production-files.d.ts.map +1 -1
  33. package/dist/deps/server-externals/server-production-files.js +17 -24
  34. package/dist/deps/server-externals/server-production-files.js.map +1 -1
  35. package/dist/electron/electron.d.ts.map +1 -1
  36. package/dist/electron/electron.js +11 -6
  37. package/dist/electron/electron.js.map +1 -1
  38. package/dist/utils/package-utils.d.ts +1 -1
  39. package/dist/utils/package-utils.js +1 -1
  40. package/dist/utils/workspace-utils.d.ts +2 -2
  41. package/dist/utils/workspace-utils.d.ts.map +1 -1
  42. package/dist/utils/workspace-utils.js +7 -6
  43. package/dist/utils/workspace-utils.js.map +1 -1
  44. package/package.json +9 -9
  45. package/src/capacitor/capacitor-icon.ts +2 -2
  46. package/src/capacitor/capacitor-npm-config.ts +12 -6
  47. package/src/capacitor/capacitor.ts +2 -2
  48. package/src/commands/device.ts +2 -2
  49. package/src/commands/init/generators/root.ts +1 -0
  50. package/src/commands/init/init-client.ts +1 -1
  51. package/src/commands/init/init.ts +2 -2
  52. package/src/commands/init/templates/workspace-root/mise.toml.hbs +1 -1
  53. package/src/commands/init/templates/workspace-root/package.json.hbs +0 -1
  54. package/src/commands/init/templates/workspace-root/pnpm-workspace.yaml +3 -0
  55. package/src/commands/publish/npm-publisher.ts +3 -3
  56. package/src/commands/publish/publish-command.ts +1 -1
  57. package/src/commands/publish/version-upgrade.ts +0 -51
  58. package/src/deps/replace-deps/collect-deps.ts +1 -1
  59. package/src/deps/replace-deps/replace-deps-resolve.ts +2 -2
  60. package/src/deps/replace-deps/replace-deps.ts +2 -2
  61. package/src/deps/server-externals/server-production-files.ts +18 -31
  62. package/src/electron/electron.ts +13 -7
  63. package/src/utils/package-utils.ts +1 -1
  64. package/src/utils/workspace-utils.ts +7 -10
  65. package/tests/capacitor/capacitor-npm-config.acc.spec.ts +114 -114
  66. package/tests/commands/publish-manifest.acc.spec.ts +3 -3
  67. package/tests/deps/server-externals/server-production-files.spec.ts +26 -28
  68. package/tests/electron/electron.spec.ts +13 -13
  69. package/tests/utils/workspace-utils.spec.ts +6 -9
  70. package/tests/ts-compiler/fixtures/non-angular-pkg/.cache/typecheck-browser.tsbuildinfo +0 -1
  71. package/tests/ts-compiler/fixtures/non-angular-pkg/.cache/typecheck-node.tsbuildinfo +0 -1
  72. package/tests/ts-compiler/fixtures/non-angular-pkg/.cache/typecheck.tsbuildinfo +0 -1
@@ -59,7 +59,7 @@ export interface ReplaceDepEntry {
59
59
  /**
60
60
  * 프로젝트 루트 및 워크스페이스 패키지 경로를 수집한다.
61
61
  *
62
- * package.json#workspaces를 기준으로 워크스페이스 패키지의 절대 경로를 수집한다.
62
+ * pnpm-workspace.yaml를 기준으로 워크스페이스 패키지의 절대 경로를 수집한다.
63
63
  * 설정이 없거나 파싱 실패 시 루트 경로만 반환한다.
64
64
  *
65
65
  * @param projectRoot - 프로젝트 루트 경로
@@ -82,7 +82,7 @@ export function collectSearchRoots(projectRoot: string): string[] {
82
82
  /**
83
83
  * replaceDeps 설정에서 모든 교체 대상 항목을 해결한다.
84
84
  *
85
- * 1. package.json#workspaces 파싱 → 워크스페이스 패키지 경로
85
+ * 1. pnpm-workspace.yaml 파싱 → 워크스페이스 패키지 경로
86
86
  * 2. [루트, ...워크스페이스 패키지] node_modules에서 매칭 패키지 탐색
87
87
  * 3. 패턴 매칭 + 소스 경로 존재 확인 + symlink 해결
88
88
  *
@@ -160,7 +160,7 @@ export interface WatchReplaceDepResult {
160
160
  /**
161
161
  * replaceDeps 설정에 따라 node_modules의 패키지를 소스 디렉토리로 교체한다.
162
162
  *
163
- * 1. package.json#workspaces 파싱 → 워크스페이스 패키지 경로
163
+ * 1. pnpm-workspace.yaml 파싱 → 워크스페이스 패키지 경로
164
164
  * 2. [루트, ...워크스페이스 패키지] node_modules에서 매칭 패키지 탐색
165
165
  * 3. 소스 package.json의 files 필드 + npm 기본 파일만 대상 경로에 복사 (package.json 제외)
166
166
  *
@@ -207,7 +207,7 @@ export async function setupReplaceDeps(
207
207
  /**
208
208
  * replaceDeps 설정에 따라 소스 디렉토리를 감시하고 변경사항을 대상 경로에 복사한다.
209
209
  *
210
- * 1. package.json#workspaces 파싱 → 워크스페이스 패키지 경로
210
+ * 1. pnpm-workspace.yaml 파싱 → 워크스페이스 패키지 경로
211
211
  * 2. [루트, ...워크스페이스 패키지] node_modules에서 매칭 패키지 탐색
212
212
  * 3. FsWatcher로 files 항목 경로만 감시 (300ms 딜레이)
213
213
  * 4. 변경사항을 대상 경로에 복사
@@ -29,43 +29,30 @@ export function collectAllExternals(
29
29
  };
30
30
  }
31
31
 
32
- interface BunLockFile {
33
- packages?: Record<string, unknown>;
34
- }
35
-
36
- function parsePackageReferenceVersion(reference: string): string | undefined {
37
- const normalized = reference.split("(")[0];
38
- const match = /^(.+?)@(\d[^@]*)$/.exec(normalized);
39
- return match?.[2];
40
- }
41
-
42
- function parsePackageKeyName(key: string): string {
43
- const match = /^(.+?)@(\d.+)$/.exec(key);
44
- return match?.[1] ?? key;
45
- }
46
-
47
32
  /**
48
- * bun.lock의 packages 섹션을 파싱하여 name→version 맵을 생성한다.
49
- * 키 형태는 일반 패키지명 또는 alias이고, 실제 버전은 package entry의 resolution에서 확인한다.
33
+ * pnpm-lock.yaml의 packages 섹션을 파싱하여 name→version 맵을 생성한다.
34
+ * 키 형태: "name@version" · "@scope/name@version" · "name@version(peer@ver)..."
50
35
  */
51
36
  export function parseLockfileVersions(cwd: string): Map<string, string> {
52
- const lockfilePath = path.join(cwd, "bun.lock");
37
+ const lockfilePath = path.join(cwd, "pnpm-lock.yaml");
53
38
  if (!fsx.existsSync(lockfilePath)) {
54
- throw new Error(`bun.lock not found in ${cwd}. Run "bun install" first.`);
39
+ throw new Error(`pnpm-lock.yaml not found in ${cwd}. Run "pnpm install" first.`);
55
40
  }
56
41
 
57
42
  const content = fsx.readSync(lockfilePath);
58
- const parsed = YAML.parse(content) as BunLockFile;
43
+ const parsed = YAML.parse(content) as { packages?: Record<string, unknown> };
59
44
  const map = new Map<string, string>();
60
45
 
61
- for (const [key, entry] of Object.entries(parsed.packages ?? {})) {
62
- if (!Array.isArray(entry) || typeof entry[0] !== "string") continue;
63
- const version = parsePackageReferenceVersion(entry[0]);
64
- if (version == null) continue;
65
-
66
- const keyName = parsePackageKeyName(key);
67
- if (!map.has(keyName)) {
68
- map.set(keyName, version);
46
+ for (const key of Object.keys(parsed.packages ?? {})) {
47
+ // 첫 번째 @숫자 기준으로 name / version 분리 (scope 패키지의 선두 @ 보존)
48
+ const m = /^(.+?)@(\d.+)$/.exec(key);
49
+ if (m == null) continue;
50
+ const name = m[1];
51
+ // peerDep suffix "(peer@ver)..." 제거
52
+ const parenIdx = m[2].indexOf("(");
53
+ const version = parenIdx === -1 ? m[2] : m[2].substring(0, parenIdx);
54
+ if (!map.has(name)) {
55
+ map.set(name, version);
69
56
  }
70
57
  }
71
58
 
@@ -73,7 +60,7 @@ export function parseLockfileVersions(cwd: string): Map<string, string> {
73
60
  }
74
61
 
75
62
  /**
76
- * bun.lock에서 주어진 모든 패키지의 잠긴 버전을 확인한다.
63
+ * pnpm-lock.yaml에서 주어진 모든 패키지의 잠긴 버전을 확인한다.
77
64
  * lockfile에서 패키지를 찾을 수 없으면 에러를 던진다.
78
65
  */
79
66
  export function resolveLockedVersions(cwd: string, pkgNames: string[]): Record<string, string> {
@@ -83,8 +70,8 @@ export function resolveLockedVersions(cwd: string, pkgNames: string[]): Record<s
83
70
  const version = versionMap.get(name);
84
71
  if (version == null) {
85
72
  throw new Error(
86
- `External dependency "${name}" not found in bun.lock. ` +
87
- `Run "bun install" and try again.`,
73
+ `External dependency "${name}" not found in pnpm-lock.yaml. ` +
74
+ `Run "pnpm install" and try again.`,
88
75
  );
89
76
  }
90
77
  result[name] = version;
@@ -68,14 +68,20 @@ export class Electron {
68
68
  await this._setupNpmConf();
69
69
  Electron._logger.debug("package.json 설정 완료");
70
70
 
71
- Electron._logger.debug("bun install 시작");
72
- await this._exec("bun", ["install"], this._srcPath);
73
- Electron._logger.debug("bun install 완료");
71
+ // pnpm-workspace.yaml 생성 (상위 workspace 탐색 차단)
72
+ const workspaceYamlPath = pathx.posixResolve(this._srcPath, "pnpm-workspace.yaml");
73
+ if (!(await fsx.exists(workspaceYamlPath))) {
74
+ await fsx.write(workspaceYamlPath, "");
75
+ }
76
+
77
+ Electron._logger.debug("pnpm install 시작");
78
+ await this._exec("pnpm", ["install", "--config.dangerously-allow-all-builds=true"], this._srcPath);
79
+ Electron._logger.debug("pnpm install 완료");
74
80
 
75
81
  const reinstallDeps = this._config.reinstallDependencies ?? [];
76
82
  if (reinstallDeps.length > 0) {
77
83
  Electron._logger.debug(`electron-rebuild 시작 (${reinstallDeps.join(", ")})`);
78
- await this._exec("bun", ["run", "electron-rebuild"], this._srcPath);
84
+ await this._exec("pnpm", ["exec", "electron-rebuild"], this._srcPath);
79
85
  Electron._logger.debug("electron-rebuild 완료");
80
86
  }
81
87
  Electron._logger.success("initialize 완료");
@@ -96,7 +102,7 @@ export class Electron {
96
102
 
97
103
  const spawnElectron = () => {
98
104
  Electron._logger.debug("Electron 프로세스 시작");
99
- currentElectron = shellSpawn("bun", ["run", "electron", "."], {
105
+ currentElectron = shellSpawn("pnpm", ["exec", "electron", "."], {
100
106
  cwd: this._srcPath,
101
107
  stdio: "inherit",
102
108
  reject: false,
@@ -379,8 +385,8 @@ export class Electron {
379
385
 
380
386
  Electron._logger.debug(`electron-builder 설정: ${configFilePath}`);
381
387
  await this._exec(
382
- "bun",
383
- ["run", "electron-builder", "--win", "--config", configFilePath],
388
+ "pnpm",
389
+ ["exec", "electron-builder", "--win", "--config", configFilePath],
384
390
  this._srcPath,
385
391
  );
386
392
  }
@@ -20,7 +20,7 @@ export function findPackageRoot(startDir: string): string {
20
20
  }
21
21
 
22
22
  /**
23
- * package.json#workspaces의 모든 워크스페이스 패키지를 탐색한다.
23
+ * pnpm-workspace.yaml의 모든 워크스페이스 패키지를 탐색한다.
24
24
  * 디렉토리명 → 상대 경로의 맵을 반환한다 (예: "orm" → "tests/orm", "sd" → "plugins/sd").
25
25
  * packages/·tests/·plugins/ 등 종류를 가리지 않고 포함하며, 배포/검사 대상 구분은 소비자가 relPath로 판단한다.
26
26
  */
@@ -1,6 +1,7 @@
1
1
  import fs from "fs";
2
2
  import path from "path";
3
3
  import { globSync } from "glob";
4
+ import YAML from "yaml";
4
5
  import { pathx } from "@simplysm/core-node";
5
6
 
6
7
  export interface WorkspacePackageDir {
@@ -10,16 +11,12 @@ export interface WorkspacePackageDir {
10
11
  absPath: string;
11
12
  }
12
13
 
13
- interface WorkspaceRootPackageJson {
14
- workspaces?: unknown;
15
- }
16
-
17
14
  interface WorkspacePackageJson {
18
15
  name?: unknown;
19
16
  }
20
17
 
21
18
  /**
22
- * root package.json의 workspaces 값을 Bun workspace 패턴 배열로 정규화한다.
19
+ * 워크스페이스 정의 값(배열 또는 { packages: [...] })을 패턴 배열로 정규화한다.
23
20
  */
24
21
  export function parsePackageJsonWorkspaces(workspaces: unknown): string[] {
25
22
  const rawPatterns = Array.isArray(workspaces)
@@ -35,11 +32,11 @@ export function parsePackageJsonWorkspaces(workspaces: unknown): string[] {
35
32
  }
36
33
 
37
34
  export function readWorkspacePatterns(workspaceRoot: string): string[] {
38
- const packageJsonPath = path.join(workspaceRoot, "package.json");
39
- if (!fs.existsSync(packageJsonPath)) return [];
35
+ const workspaceYamlPath = path.join(workspaceRoot, "pnpm-workspace.yaml");
36
+ if (!fs.existsSync(workspaceYamlPath)) return [];
40
37
 
41
- const pkgJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")) as WorkspaceRootPackageJson;
42
- return parsePackageJsonWorkspaces(pkgJson.workspaces);
38
+ const parsed = YAML.parse(fs.readFileSync(workspaceYamlPath, "utf-8")) as unknown;
39
+ return parsePackageJsonWorkspaces(parsed);
43
40
  }
44
41
 
45
42
  export function findWorkspaceRoot(startDir: string): string | undefined {
@@ -76,7 +73,7 @@ function splitWorkspacePatterns(patterns: string[]): { include: string[]; exclud
76
73
  }
77
74
 
78
75
  /**
79
- * Bun workspace root의 package.json#workspaces 기준으로 실제 패키지 디렉터리를 수집한다.
76
+ * 워크스페이스 root의 pnpm-workspace.yaml(packages) 기준으로 실제 패키지 디렉터리를 수집한다.
80
77
  */
81
78
  export function collectWorkspacePackages(workspaceRoot: string): WorkspacePackageDir[] {
82
79
  const root = pathx.posix(path.resolve(workspaceRoot));
@@ -1,114 +1,114 @@
1
- import { describe, it, expect, vi, beforeAll, afterAll, beforeEach } from "vitest";
2
- import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
- import { tmpdir } from "node:os";
4
- import path from "node:path";
5
- import { fsx, cpx } from "@simplysm/core-node";
6
-
7
- const mockFsxExists = vi.spyOn(fsx, "exists");
8
- vi.spyOn(fsx, "read");
9
- vi.spyOn(fsx, "write").mockResolvedValue(undefined);
10
- const mockFsxReadJson = vi.spyOn(fsx, "readJson");
11
- vi.spyOn(fsx, "writeJson").mockResolvedValue(undefined);
12
- vi.spyOn(fsx, "mkdir").mockResolvedValue(undefined);
13
-
14
- const execaCalls: { command: string; args: string[]; options: unknown }[] = [];
15
- vi.spyOn(cpx, "spawn").mockImplementation(((cmd: string, args: string[], options: unknown) => {
16
- execaCalls.push({
17
- command: cmd,
18
- args,
19
- options,
20
- });
21
- return { stdout: "", stderr: "", exitCode: 0 };
22
- }) as never);
23
-
24
- //#endregion
25
-
26
- let tmpRoot: string;
27
- let CAP_PATH: string;
28
- let PKG_PATH: string;
29
-
30
- beforeAll(() => {
31
- tmpRoot = mkdtempSync(path.join(tmpdir(), "cap-npm-config-acc-"));
32
- writeFileSync(path.join(tmpRoot, "package.json"), JSON.stringify({ private: true, workspaces: ["pkg"] }));
33
- PKG_PATH = path.join(tmpRoot, "pkg");
34
- CAP_PATH = path.join(PKG_PATH, ".capacitor");
35
- });
36
-
37
- afterAll(() => {
38
- rmSync(tmpRoot, { recursive: true, force: true });
39
- });
40
-
41
- function setupDefaultMocks() {
42
- mockFsxExists.mockImplementation(((p: string) => {
43
- const n = p.replace(/\\/g, "/");
44
- if (n.includes(".capacitor.lock")) return false;
45
- return true;
46
- }) as never);
47
-
48
- mockFsxReadJson.mockImplementation(((p: string) => {
49
- const normalized = p.replace(/\\/g, "/");
50
- if (normalized.includes(".capacitor/package.json")) {
51
- return {
52
- name: "com.test.app",
53
- version: "1.0.0",
54
- dependencies: {
55
- "@capacitor/core": "^7",
56
- "@capacitor/app": "^7",
57
- "@capacitor/android": "^7",
58
- },
59
- devDependencies: {
60
- "@capacitor/cli": "^7",
61
- "@capacitor/assets": "^3",
62
- },
63
- };
64
- }
65
- return { name: "test-pkg", version: "1.0.0" };
66
- }) as never);
67
-
68
- execaCalls.length = 0;
69
- }
70
-
71
- describe("initCapNpmProject", () => {
72
- beforeEach(() => {
73
- vi.clearAllMocks();
74
- setupDefaultMocks();
75
- });
76
-
77
- it("의존성 미변경 + node_modules 존재 시 false를 반환하고 bun install을 실행하지 않는다", async () => {
78
- const { initCapNpmProject } = await import(
79
- "../../src/capacitor/capacitor-npm-config.js"
80
- );
81
-
82
- const changed = await initCapNpmProject(CAP_PATH, PKG_PATH, {
83
- appId: "com.test.app",
84
- appName: "Test App",
85
- platform: { android: {} },
86
- }, { name: "test-pkg", version: "1.0.0" }, ["android"], []);
87
-
88
- expect(changed).toBe(false);
89
- });
90
-
91
- it("node_modules가 없으면 true를 반환하고 bun install을 실행한다", async () => {
92
- mockFsxExists.mockImplementation(((p: string) => {
93
- const n = p.replace(/\\/g, "/");
94
- if (n.includes(".capacitor.lock")) return false;
95
- if (n.includes("node_modules")) return false;
96
- return true;
97
- }) as never);
98
-
99
- const { initCapNpmProject } = await import(
100
- "../../src/capacitor/capacitor-npm-config.js"
101
- );
102
-
103
- const changed = await initCapNpmProject(CAP_PATH, PKG_PATH, {
104
- appId: "com.test.app",
105
- appName: "Test App",
106
- platform: { android: {} },
107
- }, {
108
- name: "test-pkg",
109
- version: "1.0.0",
110
- }, ["android"], []);
111
-
112
- expect(changed).toBe(true);
113
- });
114
- });
1
+ import { describe, it, expect, vi, beforeAll, afterAll, beforeEach } from "vitest";
2
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import path from "node:path";
5
+ import { fsx, cpx } from "@simplysm/core-node";
6
+
7
+ const mockFsxExists = vi.spyOn(fsx, "exists");
8
+ vi.spyOn(fsx, "read");
9
+ vi.spyOn(fsx, "write").mockResolvedValue(undefined);
10
+ const mockFsxReadJson = vi.spyOn(fsx, "readJson");
11
+ vi.spyOn(fsx, "writeJson").mockResolvedValue(undefined);
12
+ vi.spyOn(fsx, "mkdir").mockResolvedValue(undefined);
13
+
14
+ const execaCalls: { command: string; args: string[]; options: unknown }[] = [];
15
+ vi.spyOn(cpx, "spawn").mockImplementation(((cmd: string, args: string[], options: unknown) => {
16
+ execaCalls.push({
17
+ command: cmd,
18
+ args,
19
+ options,
20
+ });
21
+ return { stdout: "", stderr: "", exitCode: 0 };
22
+ }) as never);
23
+
24
+ //#endregion
25
+
26
+ let tmpRoot: string;
27
+ let CAP_PATH: string;
28
+ let PKG_PATH: string;
29
+
30
+ beforeAll(() => {
31
+ tmpRoot = mkdtempSync(path.join(tmpdir(), "cap-npm-config-acc-"));
32
+ writeFileSync(path.join(tmpRoot, "pnpm-workspace.yaml"), "packages:\n - pkg\n");
33
+ PKG_PATH = path.join(tmpRoot, "pkg");
34
+ CAP_PATH = path.join(PKG_PATH, ".capacitor");
35
+ });
36
+
37
+ afterAll(() => {
38
+ rmSync(tmpRoot, { recursive: true, force: true });
39
+ });
40
+
41
+ function setupDefaultMocks() {
42
+ mockFsxExists.mockImplementation(((p: string) => {
43
+ const n = p.replace(/\\/g, "/");
44
+ if (n.includes(".capacitor.lock")) return false;
45
+ return true;
46
+ }) as never);
47
+
48
+ mockFsxReadJson.mockImplementation(((p: string) => {
49
+ const normalized = p.replace(/\\/g, "/");
50
+ if (normalized.includes(".capacitor/package.json")) {
51
+ return {
52
+ name: "com.test.app",
53
+ version: "1.0.0",
54
+ dependencies: {
55
+ "@capacitor/core": "^7",
56
+ "@capacitor/app": "^7",
57
+ "@capacitor/android": "^7",
58
+ },
59
+ devDependencies: {
60
+ "@capacitor/cli": "^7",
61
+ "@capacitor/assets": "^3",
62
+ },
63
+ };
64
+ }
65
+ return { name: "test-pkg", version: "1.0.0" };
66
+ }) as never);
67
+
68
+ execaCalls.length = 0;
69
+ }
70
+
71
+ describe("initCapNpmProject", () => {
72
+ beforeEach(() => {
73
+ vi.clearAllMocks();
74
+ setupDefaultMocks();
75
+ });
76
+
77
+ it("의존성 미변경 + node_modules 존재 시 false를 반환하고 pnpm install을 실행하지 않는다", async () => {
78
+ const { initCapNpmProject } = await import(
79
+ "../../src/capacitor/capacitor-npm-config.js"
80
+ );
81
+
82
+ const changed = await initCapNpmProject(CAP_PATH, PKG_PATH, {
83
+ appId: "com.test.app",
84
+ appName: "Test App",
85
+ platform: { android: {} },
86
+ }, { name: "test-pkg", version: "1.0.0" }, ["android"], []);
87
+
88
+ expect(changed).toBe(false);
89
+ });
90
+
91
+ it("node_modules가 없으면 true를 반환하고 pnpm install을 실행한다", async () => {
92
+ mockFsxExists.mockImplementation(((p: string) => {
93
+ const n = p.replace(/\\/g, "/");
94
+ if (n.includes(".capacitor.lock")) return false;
95
+ if (n.includes("node_modules")) return false;
96
+ return true;
97
+ }) as never);
98
+
99
+ const { initCapNpmProject } = await import(
100
+ "../../src/capacitor/capacitor-npm-config.js"
101
+ );
102
+
103
+ const changed = await initCapNpmProject(CAP_PATH, PKG_PATH, {
104
+ appId: "com.test.app",
105
+ appName: "Test App",
106
+ platform: { android: {} },
107
+ }, {
108
+ name: "test-pkg",
109
+ version: "1.0.0",
110
+ }, ["android"], []);
111
+
112
+ expect(changed).toBe(true);
113
+ });
114
+ });
@@ -39,14 +39,14 @@ afterEach(() => {
39
39
  });
40
40
 
41
41
  describe("npm package manifest", () => {
42
- it("keeps CLI binary and replaces workspace dependency ranges in Bun pack output", async () => {
42
+ it("keeps CLI binary and replaces workspace dependency ranges in pnpm pack output", async () => {
43
43
  const pkgRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
44
44
  const destination = fs.mkdtempSync(path.join(os.tmpdir(), "sd-cli-pack-"));
45
45
  tmpDirs.push(destination);
46
46
 
47
47
  await cpx.spawn(
48
- "bun",
49
- ["pm", "pack", "--destination", destination, "--ignore-scripts", "--quiet"],
48
+ "pnpm",
49
+ ["pack", "--pack-destination", destination],
50
50
  { cwd: pkgRoot },
51
51
  );
52
52
 
@@ -10,9 +10,9 @@ import {
10
10
  const tmpDirs: string[] = [];
11
11
 
12
12
  function createLock(content: string): string {
13
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), "sd-bun-lock-"));
13
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "sd-pnpm-lock-"));
14
14
  tmpDirs.push(dir);
15
- fs.writeFileSync(path.join(dir, "bun.lock"), content);
15
+ fs.writeFileSync(path.join(dir, "pnpm-lock.yaml"), content);
16
16
  return dir;
17
17
  }
18
18
 
@@ -23,46 +23,44 @@ afterEach(() => {
23
23
  });
24
24
 
25
25
  describe("parseLockfileVersions", () => {
26
- it("reads unscoped, scoped, alias, and optional entries from bun.lock", () => {
27
- const cwd = createLock(`{
28
- "lockfileVersion": 1,
29
- "packages": {
30
- "is-number": ["is-number@7.0.0", "", {}, "sha512-a"],
31
- "@types/node": ["@types/node@24.13.2", "", {}, "sha512-b"],
32
- "alias-pkg": ["is-number@6.0.0", "", {}, "sha512-c"],
33
- "fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-d"],
34
- }
35
- }`);
26
+ it("reads unscoped and scoped entries from pnpm-lock.yaml", () => {
27
+ const cwd = createLock(`lockfileVersion: '9.0'
28
+ packages:
29
+ is-number@7.0.0:
30
+ resolution: {integrity: sha512-a}
31
+ '@types/node@24.13.2':
32
+ resolution: {integrity: sha512-b}
33
+ fsevents@2.3.3:
34
+ resolution: {integrity: sha512-d}
35
+ os: [darwin]
36
+ `);
36
37
 
37
38
  const map = parseLockfileVersions(cwd);
38
39
 
39
40
  expect(map.get("is-number")).toBe("7.0.0");
40
41
  expect(map.get("@types/node")).toBe("24.13.2");
41
- expect(map.get("alias-pkg")).toBe("6.0.0");
42
42
  expect(map.get("fsevents")).toBe("2.3.3");
43
43
  });
44
44
 
45
- it("strips peer suffix from package references", () => {
46
- const cwd = createLock(`{
47
- "lockfileVersion": 1,
48
- "packages": {
49
- "react-dom": ["react-dom@19.2.7(react@19.2.7)", "", {}, "sha512-a"],
50
- }
51
- }`);
45
+ it("strips peer suffix from package keys", () => {
46
+ const cwd = createLock(`lockfileVersion: '9.0'
47
+ packages:
48
+ 'react-dom@19.2.7(react@19.2.7)':
49
+ resolution: {integrity: sha512-a}
50
+ `);
52
51
 
53
52
  expect(parseLockfileVersions(cwd).get("react-dom")).toBe("19.2.7");
54
53
  });
55
54
  });
56
55
 
57
56
  describe("resolveLockedVersions", () => {
58
- it("throws when an external dependency is missing from bun.lock", () => {
59
- const cwd = createLock(`{
60
- "lockfileVersion": 1,
61
- "packages": {
62
- "is-number": ["is-number@7.0.0", "", {}, "sha512-a"],
63
- }
64
- }`);
57
+ it("throws when an external dependency is missing from pnpm-lock.yaml", () => {
58
+ const cwd = createLock(`lockfileVersion: '9.0'
59
+ packages:
60
+ is-number@7.0.0:
61
+ resolution: {integrity: sha512-a}
62
+ `);
65
63
 
66
- expect(() => resolveLockedVersions(cwd, ["missing"])).toThrow("bun.lock");
64
+ expect(() => resolveLockedVersions(cwd, ["missing"])).toThrow("pnpm-lock.yaml");
67
65
  });
68
66
  });
@@ -145,7 +145,7 @@ describe("Electron", () => {
145
145
  //#region Rule: Electron 프로젝트를 초기화한다
146
146
 
147
147
  describe("인수 테스트: 초기화", () => {
148
- it("package.json 생성 + bun install + electron-rebuild를 실행한다", async () => {
148
+ it("package.json 생성 + pnpm install + electron-rebuild를 실행한다", async () => {
149
149
  const { Electron } = await import("../../src/electron/electron.js");
150
150
 
151
151
  const electron = await Electron.create(PKG_PATH, {
@@ -159,13 +159,13 @@ describe("Electron", () => {
159
159
 
160
160
  const spawnCalls = mockCpxSpawn.mock.calls;
161
161
  const installCall = spawnCalls.find(
162
- (c) => c[0] === "bun" && c[1].includes("install"),
162
+ (c) => c[0] === "pnpm" && c[1].includes("install"),
163
163
  );
164
164
  expect(installCall).toBeDefined();
165
165
  expect(installCall?.[2]).toEqual(expect.objectContaining({ shell: true }));
166
166
  expect(
167
167
  spawnCalls.find(
168
- (c) => c[0] === "bun" && c[1].includes("electron-rebuild"),
168
+ (c) => c[0] === "pnpm" && c[1].includes("electron-rebuild"),
169
169
  ),
170
170
  ).toBeDefined();
171
171
  });
@@ -177,7 +177,7 @@ describe("Electron", () => {
177
177
  await electron.initialize();
178
178
 
179
179
  const rebuildCall = mockCpxSpawn.mock.calls.find(
180
- (c) => c[0] === "bun" && c[1].includes("electron-rebuild"),
180
+ (c) => c[0] === "pnpm" && c[1].includes("electron-rebuild"),
181
181
  );
182
182
  expect(rebuildCall).toBeUndefined();
183
183
  });
@@ -455,8 +455,8 @@ describe("Electron", () => {
455
455
  let resolveElectron: () => void = () => {};
456
456
 
457
457
  mockCpxSpawn.mockImplementation((cmd: string, args: string[]) => {
458
- // bun run electron . → Electron 프로세스
459
- if (cmd === "bun" && args[0] === "run" && args[1] === "electron" && args[2] === ".") {
458
+ // pnpm exec electron . → Electron 프로세스
459
+ if (cmd === "pnpm" && args[0] === "exec" && args[1] === "electron" && args[2] === ".") {
460
460
  const p = new Promise<void>((resolve) => {
461
461
  resolveElectron = resolve;
462
462
  }) as any;
@@ -499,7 +499,7 @@ describe("Electron", () => {
499
499
  expect(callArgs.bundle).toBe(true);
500
500
  expect(callArgs.external).toContain("electron");
501
501
  const electronCall = mockCpxSpawn.mock.calls.find(
502
- (c) => c[0] === "bun" && c[1].includes("electron"),
502
+ (c) => c[0] === "pnpm" && c[1].includes("electron"),
503
503
  );
504
504
  expect(electronCall?.[2]).toEqual(expect.objectContaining({ shell: true, reject: false }));
505
505
 
@@ -545,7 +545,7 @@ describe("Electron", () => {
545
545
  it("passes custom env and ELECTRON_DEV_URL via esbuild banner", async () => {
546
546
  let resolveElectron: () => void = () => {};
547
547
  mockCpxSpawn.mockImplementation((cmd: string, args: string[]) => {
548
- if (cmd === "bun" && args[0] === "run" && args[1] === "electron" && args[2] === ".") {
548
+ if (cmd === "pnpm" && args[0] === "exec" && args[1] === "electron" && args[2] === ".") {
549
549
  const p = new Promise<void>((resolve) => {
550
550
  resolveElectron = resolve;
551
551
  }) as any;
@@ -578,7 +578,7 @@ describe("Electron", () => {
578
578
  it("calls initialize() before starting esbuild context", async () => {
579
579
  let resolveElectron: () => void = () => {};
580
580
  mockCpxSpawn.mockImplementation((cmd: string, args: string[]) => {
581
- if (cmd === "bun" && args[0] === "run" && args[1] === "electron" && args[2] === ".") {
581
+ if (cmd === "pnpm" && args[0] === "exec" && args[1] === "electron" && args[2] === ".") {
582
582
  const p = new Promise<void>((resolve) => {
583
583
  resolveElectron = resolve;
584
584
  }) as any;
@@ -596,11 +596,11 @@ describe("Electron", () => {
596
596
  resolveElectron();
597
597
  await runPromise;
598
598
 
599
- // initialize calls bun install
600
- const bunInstallCall = mockCpxSpawn.mock.calls.find(
601
- (c: any[]) => c[0] === "bun" && (c[1] as string[]).includes("install"),
599
+ // initialize calls pnpm install
600
+ const pnpmInstallCall = mockCpxSpawn.mock.calls.find(
601
+ (c: any[]) => c[0] === "pnpm" && (c[1] as string[]).includes("install"),
602
602
  );
603
- expect(bunInstallCall).toBeDefined();
603
+ expect(pnpmInstallCall).toBeDefined();
604
604
  }, 10_000);
605
605
  });
606
606