@simplysm/sd-cli 14.1.11 → 14.1.12
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/capacitor/capacitor-icon.js +2 -2
- package/dist/capacitor/capacitor-icon.js.map +1 -1
- package/dist/capacitor/capacitor-npm-config.d.ts +1 -1
- package/dist/capacitor/capacitor-npm-config.d.ts.map +1 -1
- package/dist/capacitor/capacitor-npm-config.js +11 -6
- package/dist/capacitor/capacitor-npm-config.js.map +1 -1
- package/dist/capacitor/capacitor.d.ts +1 -1
- package/dist/capacitor/capacitor.js +2 -2
- package/dist/capacitor/capacitor.js.map +1 -1
- package/dist/commands/device.js +2 -2
- package/dist/commands/device.js.map +1 -1
- package/dist/commands/init/generators/root.d.ts.map +1 -1
- package/dist/commands/init/generators/root.js +1 -0
- package/dist/commands/init/generators/root.js.map +1 -1
- package/dist/commands/init/init-client.js +1 -1
- package/dist/commands/init/init-client.js.map +1 -1
- package/dist/commands/init/init.js +2 -2
- package/dist/commands/init/init.js.map +1 -1
- package/dist/commands/publish/npm-publisher.js +3 -3
- package/dist/commands/publish/npm-publisher.js.map +1 -1
- package/dist/commands/publish/publish-command.js +1 -1
- package/dist/commands/publish/publish-command.js.map +1 -1
- package/dist/commands/publish/version-upgrade.d.ts.map +1 -1
- package/dist/commands/publish/version-upgrade.js +0 -50
- package/dist/commands/publish/version-upgrade.js.map +1 -1
- package/dist/deps/replace-deps/collect-deps.js +1 -1
- package/dist/deps/replace-deps/replace-deps-resolve.d.ts +2 -2
- package/dist/deps/replace-deps/replace-deps-resolve.js +2 -2
- package/dist/deps/replace-deps/replace-deps.d.ts +2 -2
- package/dist/deps/replace-deps/replace-deps.js +2 -2
- package/dist/deps/server-externals/server-production-files.d.ts +3 -3
- package/dist/deps/server-externals/server-production-files.d.ts.map +1 -1
- package/dist/deps/server-externals/server-production-files.js +17 -24
- package/dist/deps/server-externals/server-production-files.js.map +1 -1
- package/dist/electron/electron.d.ts.map +1 -1
- package/dist/electron/electron.js +11 -6
- package/dist/electron/electron.js.map +1 -1
- package/dist/utils/package-utils.d.ts +1 -1
- package/dist/utils/package-utils.js +1 -1
- package/dist/utils/workspace-utils.d.ts +2 -2
- package/dist/utils/workspace-utils.d.ts.map +1 -1
- package/dist/utils/workspace-utils.js +7 -6
- package/dist/utils/workspace-utils.js.map +1 -1
- package/package.json +9 -9
- package/src/capacitor/capacitor-icon.ts +2 -2
- package/src/capacitor/capacitor-npm-config.ts +12 -6
- package/src/capacitor/capacitor.ts +2 -2
- package/src/commands/device.ts +2 -2
- package/src/commands/init/generators/root.ts +1 -0
- package/src/commands/init/init-client.ts +1 -1
- package/src/commands/init/init.ts +2 -2
- package/src/commands/init/templates/workspace-root/mise.toml.hbs +1 -1
- package/src/commands/init/templates/workspace-root/package.json.hbs +0 -1
- package/src/commands/init/templates/workspace-root/pnpm-workspace.yaml +3 -0
- package/src/commands/publish/npm-publisher.ts +3 -3
- package/src/commands/publish/publish-command.ts +1 -1
- package/src/commands/publish/version-upgrade.ts +0 -51
- package/src/deps/replace-deps/collect-deps.ts +1 -1
- package/src/deps/replace-deps/replace-deps-resolve.ts +2 -2
- package/src/deps/replace-deps/replace-deps.ts +2 -2
- package/src/deps/server-externals/server-production-files.ts +18 -31
- package/src/electron/electron.ts +13 -7
- package/src/utils/package-utils.ts +1 -1
- package/src/utils/workspace-utils.ts +7 -10
- package/tests/capacitor/capacitor-npm-config.acc.spec.ts +114 -114
- package/tests/deps/server-externals/server-production-files.spec.ts +26 -28
- package/tests/electron/electron.spec.ts +13 -13
- package/tests/utils/workspace-utils.spec.ts +6 -9
- package/tests/ts-compiler/fixtures/non-angular-pkg/.cache/typecheck-browser.tsbuildinfo +0 -1
- package/tests/ts-compiler/fixtures/non-angular-pkg/.cache/typecheck-node.tsbuildinfo +0 -1
- 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
|
-
*
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
*
|
|
49
|
-
* 키
|
|
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, "
|
|
37
|
+
const lockfilePath = path.join(cwd, "pnpm-lock.yaml");
|
|
53
38
|
if (!fsx.existsSync(lockfilePath)) {
|
|
54
|
-
throw new Error(`
|
|
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
|
|
43
|
+
const parsed = YAML.parse(content) as { packages?: Record<string, unknown> };
|
|
59
44
|
const map = new Map<string, string>();
|
|
60
45
|
|
|
61
|
-
for (const
|
|
62
|
-
|
|
63
|
-
const
|
|
64
|
-
if (
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
*
|
|
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
|
|
87
|
-
`Run "
|
|
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;
|
package/src/electron/electron.ts
CHANGED
|
@@ -68,14 +68,20 @@ export class Electron {
|
|
|
68
68
|
await this._setupNpmConf();
|
|
69
69
|
Electron._logger.debug("package.json 설정 완료");
|
|
70
70
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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("
|
|
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("
|
|
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
|
-
"
|
|
383
|
-
["
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
|
39
|
-
if (!fs.existsSync(
|
|
35
|
+
const workspaceYamlPath = path.join(workspaceRoot, "pnpm-workspace.yaml");
|
|
36
|
+
if (!fs.existsSync(workspaceYamlPath)) return [];
|
|
40
37
|
|
|
41
|
-
const
|
|
42
|
-
return parsePackageJsonWorkspaces(
|
|
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
|
-
*
|
|
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, "
|
|
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를 반환하고
|
|
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를 반환하고
|
|
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
|
+
});
|
|
@@ -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-
|
|
13
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "sd-pnpm-lock-"));
|
|
14
14
|
tmpDirs.push(dir);
|
|
15
|
-
fs.writeFileSync(path.join(dir, "
|
|
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
|
|
27
|
-
const cwd = createLock(`
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
|
46
|
-
const cwd = createLock(`
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
|
59
|
-
const cwd = createLock(`
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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("
|
|
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 생성 +
|
|
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] === "
|
|
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] === "
|
|
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] === "
|
|
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
|
-
//
|
|
459
|
-
if (cmd === "
|
|
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] === "
|
|
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 === "
|
|
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 === "
|
|
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
|
|
600
|
-
const
|
|
601
|
-
(c: any[]) => c[0] === "
|
|
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(
|
|
603
|
+
expect(pnpmInstallCall).toBeDefined();
|
|
604
604
|
}, 10_000);
|
|
605
605
|
});
|
|
606
606
|
|
|
@@ -46,10 +46,10 @@ describe("parsePackageJsonWorkspaces", () => {
|
|
|
46
46
|
describe("collectWorkspacePackages", () => {
|
|
47
47
|
it("collects package directories and ignores negative patterns", () => {
|
|
48
48
|
const root = createTmpRoot();
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
49
|
+
fs.writeFileSync(
|
|
50
|
+
path.join(root, "pnpm-workspace.yaml"),
|
|
51
|
+
"packages:\n - packages/*\n - tests/*\n - '!packages/skip'\n",
|
|
52
|
+
);
|
|
53
53
|
writeJson(path.join(root, "packages", "core", "package.json"), {
|
|
54
54
|
name: "@scope/core",
|
|
55
55
|
});
|
|
@@ -73,12 +73,9 @@ describe("collectWorkspacePackages", () => {
|
|
|
73
73
|
});
|
|
74
74
|
|
|
75
75
|
describe("findWorkspaceRoot", () => {
|
|
76
|
-
it("finds nearest
|
|
76
|
+
it("finds nearest pnpm-workspace.yaml", () => {
|
|
77
77
|
const root = createTmpRoot();
|
|
78
|
-
|
|
79
|
-
private: true,
|
|
80
|
-
workspaces: ["packages/*"],
|
|
81
|
-
});
|
|
78
|
+
fs.writeFileSync(path.join(root, "pnpm-workspace.yaml"), "packages:\n - packages/*\n");
|
|
82
79
|
const nested = path.join(root, "packages", "app", "src");
|
|
83
80
|
fs.mkdirSync(nested, { recursive: true });
|
|
84
81
|
|