@simplysm/sd-cli 14.0.6 → 14.0.8

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 (58) hide show
  1. package/dist/capacitor/capacitor.d.ts +1 -1
  2. package/dist/capacitor/capacitor.d.ts.map +1 -1
  3. package/dist/capacitor/capacitor.js +3 -4
  4. package/dist/capacitor/capacitor.js.map +1 -1
  5. package/dist/commands/check.js +2 -2
  6. package/dist/commands/check.js.map +1 -1
  7. package/dist/commands/publish.d.ts.map +1 -1
  8. package/dist/commands/publish.js +12 -13
  9. package/dist/commands/publish.js.map +1 -1
  10. package/dist/electron/electron.d.ts.map +1 -1
  11. package/dist/electron/electron.js +24 -19
  12. package/dist/electron/electron.js.map +1 -1
  13. package/dist/engines/BaseEngine.d.ts.map +1 -1
  14. package/dist/engines/BaseEngine.js +1 -0
  15. package/dist/engines/BaseEngine.js.map +1 -1
  16. package/dist/orchestrators/DevWatchOrchestrator.d.ts.map +1 -1
  17. package/dist/orchestrators/DevWatchOrchestrator.js +9 -3
  18. package/dist/orchestrators/DevWatchOrchestrator.js.map +1 -1
  19. package/dist/sd-cli.js +6 -6
  20. package/dist/sd-cli.js.map +1 -1
  21. package/dist/utils/esbuild-config.d.ts +7 -2
  22. package/dist/utils/esbuild-config.d.ts.map +1 -1
  23. package/dist/utils/esbuild-config.js +15 -12
  24. package/dist/utils/esbuild-config.js.map +1 -1
  25. package/dist/utils/vite-config.d.ts.map +1 -1
  26. package/dist/utils/vite-config.js +1 -2
  27. package/dist/utils/vite-config.js.map +1 -1
  28. package/dist/vitest-plugin.d.ts.map +1 -1
  29. package/dist/vitest-plugin.js +2 -0
  30. package/dist/vitest-plugin.js.map +1 -1
  31. package/dist/workers/server-build.worker.d.ts.map +1 -1
  32. package/dist/workers/server-build.worker.js +2 -3
  33. package/dist/workers/server-build.worker.js.map +1 -1
  34. package/package.json +4 -6
  35. package/src/capacitor/capacitor.ts +3 -4
  36. package/src/commands/check.ts +2 -2
  37. package/src/commands/publish.ts +12 -13
  38. package/src/electron/electron.ts +28 -21
  39. package/src/engines/BaseEngine.ts +1 -0
  40. package/src/orchestrators/DevWatchOrchestrator.ts +10 -3
  41. package/src/sd-cli.ts +9 -6
  42. package/src/utils/esbuild-config.ts +16 -12
  43. package/src/utils/vite-config.ts +1 -2
  44. package/src/vitest-plugin.ts +5 -0
  45. package/src/workers/server-build.worker.ts +2 -3
  46. package/tests/capacitor/capacitor-build.spec.ts +9 -7
  47. package/tests/capacitor/capacitor-icon.spec.ts +9 -7
  48. package/tests/capacitor/capacitor-init.spec.ts +8 -6
  49. package/tests/capacitor/capacitor-run.spec.ts +13 -11
  50. package/tests/capacitor/capacitor-workspace.spec.ts +8 -6
  51. package/tests/commands/check.spec.ts +16 -28
  52. package/tests/commands/publish.spec.ts +4 -4
  53. package/tests/electron/electron.spec.ts +69 -58
  54. package/tests/orchestrators/build-orchestrator.spec.ts +4 -9
  55. package/tests/orchestrators/dev-watch-orchestrator.spec.ts +3 -5
  56. package/tests/utils/esbuild-config.spec.ts +38 -8
  57. package/tests/utils/vite-config.spec.ts +13 -0
  58. package/tests/workers/server-build-worker.spec.ts +6 -3
@@ -1,10 +1,9 @@
1
1
  import fs from "node:fs";
2
2
  import { existsSync } from "node:fs";
3
3
  import path from "path";
4
- import { execa } from "execa";
5
4
  import { symlink } from "fs/promises";
6
5
  import { createRequire } from "module";
7
- import { fsx } from "@simplysm/core-node";
6
+ import { cpx, fsx } from "@simplysm/core-node";
8
7
  import { env } from "@simplysm/core-common";
9
8
  import { consola } from "consola";
10
9
  import type { SdCapacitorConfig } from "../sd-config.types.js";
@@ -154,11 +153,11 @@ export class Capacitor {
154
153
  }
155
154
 
156
155
  /**
157
- * 명령어 실행 (execa — Windows .cmd 자동 resolve)
156
+ * 명령어 실행
158
157
  */
159
158
  private async _exec(command: string, args: string[], cwd: string): Promise<string> {
160
159
  Capacitor._logger.debug(`명령어 실행: ${command} ${args.join(" ")}`);
161
- const { stdout } = await execa(command, args, { cwd });
160
+ const { stdout } = await cpx.exec(command, args, { cwd });
162
161
  Capacitor._logger.debug(`실행 결과: ${stdout}`);
163
162
  return stdout;
164
163
  }
@@ -1,4 +1,4 @@
1
- import { execa } from "execa";
1
+ import { cpx } from "@simplysm/core-node";
2
2
  import { err as errNs } from "@simplysm/core-common";
3
3
  import { executeTypecheck, type TypecheckResult } from "./typecheck";
4
4
  import { executeLint, type LintResult } from "./lint";
@@ -34,7 +34,7 @@ async function spawnVitest(targets: string[]): Promise<CheckResult> {
34
34
  const args = ["vitest", ...targets, "--run"];
35
35
  logger.debug("vitest 실행", { args });
36
36
  logger.start("테스트 실행 중...");
37
- const result = await execa("pnpm", args, { cwd: process.cwd(), reject: false });
37
+ const result = await cpx.exec("pnpm", args, { cwd: process.cwd(), reject: false });
38
38
  const output = result.stdout + result.stderr;
39
39
  const code = result.exitCode;
40
40
 
@@ -2,13 +2,12 @@ import path from "path";
2
2
  import semver from "semver";
3
3
  import { consola } from "consola";
4
4
  import { StorageFactory } from "@simplysm/storage";
5
- import { fsx } from "@simplysm/core-node";
5
+ import { cpx, fsx } from "@simplysm/core-node";
6
6
  import { env, json } from "@simplysm/core-common";
7
7
  import "@simplysm/core-common";
8
8
  import type { SdConfig, SdPublishConfig } from "../sd-config.types";
9
9
  import { loadSdConfig } from "../utils/sd-config";
10
10
  import { validateTargets } from "../utils/package-utils";
11
- import { execa } from "execa";
12
11
  import { runBuild } from "./build";
13
12
  import { parseWorkspaceGlobs } from "../utils/replace-deps";
14
13
  import os from "os";
@@ -353,7 +352,7 @@ async function publishPackage(
353
352
  logger.debug(`[${pkgName}] pnpm ${args.join(" ")}`);
354
353
  }
355
354
 
356
- await execa("pnpm", args, { cwd: pkgPath });
355
+ await cpx.exec("pnpm", args, { cwd: pkgPath });
357
356
  } else if (publishConfig.type === "local-directory") {
358
357
  // 로컬 디렉토리에 복사
359
358
  const targetPath = replaceEnvVariables(publishConfig.path, version, projectPath);
@@ -560,7 +559,7 @@ export async function runPublish(options: PublishOptions): Promise<void> {
560
559
  if (publishPackages.some((p) => p.config.type === "npm")) {
561
560
  logger.debug("npm 인증 검증 중...");
562
561
  try {
563
- const { stdout: whoami } = await execa("npm", ["whoami"]);
562
+ const { stdout: whoami } = await cpx.exec("npm", ["whoami"]);
564
563
  if (whoami.trim() === "") {
565
564
  throw new Error("npm 로그인 정보를 찾을 수 없습니다.");
566
565
  }
@@ -590,13 +589,13 @@ export async function runPublish(options: PublishOptions): Promise<void> {
590
589
  if (!noBuild && hasGit) {
591
590
  logger.debug("git 커밋 상태 확인 중...");
592
591
  try {
593
- const { stdout: diff } = await execa("git", ["diff", "--name-only"]);
594
- const { stdout: stagedDiff } = await execa("git", ["diff", "--cached", "--name-only"]);
592
+ const { stdout: diff } = await cpx.exec("git", ["diff", "--name-only"]);
593
+ const { stdout: stagedDiff } = await cpx.exec("git", ["diff", "--cached", "--name-only"]);
595
594
 
596
595
  if (diff.trim() !== "" || stagedDiff.trim() !== "") {
597
596
  logger.info("커밋되지 않은 변경사항 감지. claude로 자동 커밋 시도 중...");
598
597
  try {
599
- await execa("claude", [
598
+ await cpx.exec("claude", [
600
599
  "-p",
601
600
  "/sd-commit all",
602
601
  "--dangerously-skip-permissions",
@@ -684,11 +683,11 @@ export async function runPublish(options: PublishOptions): Promise<void> {
684
683
  } else {
685
684
  logger.debug("Git commit/tag/push...");
686
685
  try {
687
- await execa("git", ["add", ..._changedFiles]);
688
- await execa("git", ["commit", "-m", `v${version}`]);
689
- await execa("git", ["tag", "-a", `v${version}`, "-m", `v${version}`]);
690
- await execa("git", ["push"]);
691
- await execa("git", ["push", "--tags"]);
686
+ await cpx.exec("git", ["add", ..._changedFiles]);
687
+ await cpx.exec("git", ["commit", "-m", `v${version}`]);
688
+ await cpx.exec("git", ["tag", "-a", `v${version}`, "-m", `v${version}`]);
689
+ await cpx.exec("git", ["push"]);
690
+ await cpx.exec("git", ["push", "--tags"]);
692
691
  logger.debug("Git 작업 완료");
693
692
  } catch (err) {
694
693
  logger.error(
@@ -807,7 +806,7 @@ export async function runPublish(options: PublishOptions): Promise<void> {
807
806
  logger.info(`[DRY-RUN] 실행 예정: ${cmd} ${args.join(" ")}`);
808
807
  } else {
809
808
  logger.debug(`실행 중: ${cmd} ${args.join(" ")}`);
810
- await execa(cmd, args, { cwd });
809
+ await cpx.exec(cmd, args, { cwd });
811
810
  }
812
811
  } catch (err) {
813
812
  // postPublish 실패 시 경고만 출력 (배포 롤백 불가)
@@ -2,10 +2,10 @@ import path from "path";
2
2
  import os from "os";
3
3
  import fs from "fs";
4
4
  import module from "module";
5
- import { fsx } from "@simplysm/core-node";
5
+ import { cpx, fsx } from "@simplysm/core-node";
6
6
  import { consola } from "consola";
7
- import { execa } from "execa";
8
7
  import type { SdElectronConfig } from "../sd-config.types.js";
8
+ import { createEnvBanner } from "../utils/esbuild-config.js";
9
9
 
10
10
  interface NpmConfig {
11
11
  name: string;
@@ -56,7 +56,7 @@ export class Electron {
56
56
  env?: Record<string, string>,
57
57
  ): Promise<string> {
58
58
  Electron._logger.debug(`실행: ${cmd} ${args.join(" ")}`);
59
- const { stdout: result } = await execa(cmd, args, { cwd, env: { ...process.env, ...env } });
59
+ const { stdout: result } = await cpx.exec(cmd, args, { cwd, env });
60
60
  Electron._logger.debug(`결과: ${result}`);
61
61
  return result;
62
62
  }
@@ -80,12 +80,6 @@ export class Electron {
80
80
 
81
81
  await this.initialize();
82
82
 
83
- const runEnv: Record<string, string> = {
84
- NODE_ENV: "development",
85
- ELECTRON_DEV_URL: url,
86
- ...this._config.env,
87
- };
88
-
89
83
  const esbuild = await import("esbuild");
90
84
  const entryPoint = path.resolve(this._pkgPath, "src/electron-main.ts");
91
85
 
@@ -97,14 +91,13 @@ export class Electron {
97
91
  const reinstallDeps = this._config.reinstallDependencies ?? [];
98
92
  await fsx.mkdir(srcPath);
99
93
 
100
- let currentElectron: import("execa").ResultPromise | null = null;
94
+ let currentElectron: cpx.ExecProcess | null = null;
101
95
  let isRestarting = false;
102
96
  let resolveTermination: (() => void) | null = null;
103
97
 
104
98
  const spawnElectron = () => {
105
- currentElectron = execa(this._localBin("electron"), ["."], {
99
+ currentElectron = cpx.exec(this._localBin("electron"), ["."], {
106
100
  cwd: srcPath,
107
- env: { ...process.env, ...runEnv },
108
101
  stdio: "inherit",
109
102
  reject: false,
110
103
  });
@@ -118,6 +111,8 @@ export class Electron {
118
111
  });
119
112
  };
120
113
 
114
+ const envBanner = createEnvBanner({ ELECTRON_DEV_URL: url, ...this._config.env });
115
+
121
116
  const ctx = await esbuild.context({
122
117
  entryPoints: [entryPoint],
123
118
  outfile: path.resolve(srcPath, "electron-main.js"),
@@ -125,7 +120,8 @@ export class Electron {
125
120
  target: "node20",
126
121
  format: "cjs",
127
122
  bundle: true,
128
- external: ["electron", ...builtinModules, ...reinstallDeps],
123
+ external: ["electron", ...builtinModules, ...reinstallDeps, ...this._exclude],
124
+ banner: { js: envBanner },
129
125
  plugins: [
130
126
  {
131
127
  name: "electron-restart",
@@ -158,15 +154,22 @@ export class Electron {
158
154
  await ctx.watch();
159
155
 
160
156
  await new Promise<void>((resolve) => {
161
- resolveTermination = () => {
157
+ let disposed = false;
158
+
159
+ const cleanup = () => {
160
+ if (disposed) return;
161
+ disposed = true;
162
+ process.removeListener("SIGINT", signalHandler);
163
+ process.removeListener("SIGTERM", signalHandler);
162
164
  void ctx.dispose();
163
165
  resolve();
164
166
  };
165
167
 
168
+ resolveTermination = cleanup;
169
+
166
170
  const signalHandler = () => {
167
171
  if (currentElectron != null) currentElectron.kill();
168
- void ctx.dispose();
169
- resolve();
172
+ cleanup();
170
173
  };
171
174
 
172
175
  process.once("SIGINT", signalHandler);
@@ -241,6 +244,8 @@ export class Electron {
241
244
 
242
245
  await fsx.mkdir(outDir);
243
246
 
247
+ const envBanner = createEnvBanner(this._config.env);
248
+
244
249
  await esbuild.build({
245
250
  entryPoints: [entryPoint],
246
251
  outfile: path.resolve(outDir, "electron-main.js"),
@@ -248,7 +253,8 @@ export class Electron {
248
253
  target: "node20",
249
254
  format: "cjs",
250
255
  bundle: true,
251
- external: ["electron", ...builtinModules, ...reinstallDeps],
256
+ external: ["electron", ...builtinModules, ...reinstallDeps, ...this._exclude],
257
+ banner: { js: envBanner },
252
258
  });
253
259
  }
254
260
 
@@ -295,7 +301,7 @@ export class Electron {
295
301
 
296
302
  const builderConfig: Record<string, unknown> = {
297
303
  appId: this._config.appId,
298
- productName: this._npmConfig.description,
304
+ productName: this._npmConfig.description ?? this._npmConfig.name,
299
305
  asar: false,
300
306
  win: {
301
307
  target: this._config.portable === true ? "portable" : "nsis",
@@ -329,12 +335,13 @@ export class Electron {
329
335
  const electronOutPath = path.resolve(outPath, "electron");
330
336
  await fsx.mkdir(electronOutPath);
331
337
 
332
- const description = this._npmConfig.description ?? this._npmConfig.name;
338
+ const rawName = this._npmConfig.description ?? this._npmConfig.name;
339
+ const safeName = rawName.replace(/[<>:"/\\|?*]/g, "");
333
340
  const version = this._npmConfig.version;
334
341
  const isPortable = this._config.portable === true;
335
342
 
336
343
  // exe 파일 동적 탐색 — Setup 또는 portable exe를 우선 선택
337
- const allExeFiles = await fsx.glob("*.exe", { cwd: distPath });
344
+ const allExeFiles = await fsx.glob(path.resolve(distPath, "*.exe"));
338
345
  if (allExeFiles.length === 0) {
339
346
  Electron._logger.warn(`빌드 산출물(.exe)을 찾을 수 없습니다: ${distPath}`);
340
347
  return;
@@ -343,7 +350,7 @@ export class Electron {
343
350
  const sourcePath =
344
351
  allExeFiles.find((f) => f.toLowerCase().includes(keyword.toLowerCase())) ?? allExeFiles[0];
345
352
 
346
- const latestFileName = `${description}${isPortable ? "-portable" : ""}-latest.exe`;
353
+ const latestFileName = `${safeName}${isPortable ? "-portable" : ""}-latest.exe`;
347
354
  await fsx.copy(sourcePath, path.resolve(electronOutPath, latestFileName));
348
355
 
349
356
  const updatesPath = path.resolve(electronOutPath, "updates");
@@ -184,6 +184,7 @@ export abstract class BaseEngine<
184
184
 
185
185
  if (isInitialBuild) {
186
186
  isInitialBuild = false;
187
+ logger.debug(`[${this._pkg.name}] 초기 빌드 완료 (success: ${event.js.success})`);
187
188
  resolve();
188
189
  }
189
190
  });
@@ -289,9 +289,16 @@ export class DevWatchOrchestrator {
289
289
  }
290
290
 
291
291
  // Start all engines
292
- this._logger.start("초기 빌드 실행 중...");
293
- const watchPromises: Array<Promise<void>> = [];
294
- watchPromises.push(...this._libraryEngines.map((e) => e.startWatch({ js: true, dts: true, lint: true })));
292
+ const total = this._libraryEngines.length;
293
+ this._logger.start(`초기 빌드 실행 중... (${total}개 패키지)`);
294
+ let completed = 0;
295
+
296
+ const watchPromises = this._libraryEngines.map(async (engine, i) => {
297
+ const pkgName = this._libraryPackages[i].name;
298
+ await engine.startWatch({ js: true, dts: true, lint: true });
299
+ completed++;
300
+ this._logger.info(` [${completed}/${total}] ${pkgName} 완료`);
301
+ });
295
302
 
296
303
  await Promise.allSettled(watchPromises);
297
304
  this._logger.success("초기 빌드 실행 완료");
package/src/sd-cli.ts CHANGED
@@ -7,7 +7,7 @@
7
7
  * .js execution (production): run replaceDeps then spawn sd-cli-entry in new process
8
8
  */
9
9
 
10
- import { execa } from "execa";
10
+ import { cpx } from "@simplysm/core-node";
11
11
  import os from "os";
12
12
  import path from "path";
13
13
  import { fileURLToPath } from "url";
@@ -31,7 +31,7 @@ if (isDev) {
31
31
  const { loadSdConfig } = await import("./utils/sd-config.js");
32
32
  const { setupReplaceDeps } = await import("./utils/replace-deps.js");
33
33
  const sdConfig = await loadSdConfig({ cwd: process.cwd(), dev: false, options: [] });
34
- if (sdConfig.replaceDeps != null) {
34
+ if (process.argv[2] !== "replace-deps" && sdConfig.replaceDeps != null) {
35
35
  await setupReplaceDeps(process.cwd(), sdConfig.replaceDeps);
36
36
  }
37
37
  } catch {
@@ -40,7 +40,7 @@ if (isDev) {
40
40
 
41
41
  // Phase 2: Run actual CLI in new process (reset module cache)
42
42
  const cliEntryFilePath = path.join(__dirname, "sd-cli-entry.js");
43
- const subprocess = execa(
43
+ const subprocess = cpx.exec(
44
44
  "node",
45
45
  [
46
46
  "--max-old-space-size=8192",
@@ -52,7 +52,7 @@ if (isDev) {
52
52
  );
53
53
  if (subprocess.pid != null) configureAffinityAndPriority(subprocess.pid);
54
54
  const result = await subprocess;
55
- process.exitCode = result.exitCode ?? 0;
55
+ process.exitCode = result.exitCode;
56
56
  }
57
57
 
58
58
  /**
@@ -94,8 +94,11 @@ function configureAffinityAndPriority(pid: number): void {
94
94
  command = `taskset -p ${mask} ${pid} && renice +10 -p ${pid}`;
95
95
  }
96
96
 
97
- execa({ shell: true })`${command}`.catch((err: Error) => {
97
+ cpx.exec(command, [], { shell: true }).catch((err: unknown) => {
98
98
  // eslint-disable-next-line no-console
99
- console.warn("Failed to configure CPU affinity/priority:", err.message);
99
+ console.warn(
100
+ "Failed to configure CPU affinity/priority:",
101
+ err instanceof Error ? err.message : String(err),
102
+ );
100
103
  });
101
104
  }
@@ -57,22 +57,29 @@ export interface ServerEsbuildOptions {
57
57
  dev?: boolean;
58
58
  }
59
59
 
60
+ /**
61
+ * Generate a JS banner snippet that merges env vars into process.env at runtime.
62
+ * Uses ??= so that runtime ENV (e.g. `DEV=false node server.js`) takes precedence
63
+ * over build-time defaults.
64
+ */
65
+ export function createEnvBanner(env?: Record<string, string>): string {
66
+ if (env == null || Object.keys(env).length === 0) return "";
67
+ return `for(const[__k,__v]of Object.entries(${JSON.stringify(env)})){process.env[__k]??=__v;}`;
68
+ }
69
+
60
70
  /**
61
71
  * Create esbuild config for Server build
62
72
  *
63
73
  * Used for server package builds
64
74
  * - bundle: true (single bundle with all dependencies)
65
75
  * - minify: true (minify for code protection)
66
- * - banner: createRequire shim for CJS package require() support
67
- * - Replace env with define option (process.env.KEY format)
76
+ * - banner: createRequire shim for CJS package require() support + env injection
68
77
  */
69
78
  export function createServerEsbuildOptions(options: ServerEsbuildOptions): esbuild.BuildOptions {
70
- const define: Record<string, string> = {};
71
- if (options.env != null) {
72
- for (const [key, value] of Object.entries(options.env)) {
73
- define[`process.env.${key}`] = JSON.stringify(value);
74
- }
75
- }
79
+ const envBanner = createEnvBanner(options.env);
80
+ const bannerJs =
81
+ "import { createRequire } from 'module'; const require = createRequire(import.meta.url);" +
82
+ envBanner;
76
83
 
77
84
  return {
78
85
  entryPoints: options.entryPoints,
@@ -82,11 +89,8 @@ export function createServerEsbuildOptions(options: ServerEsbuildOptions): esbui
82
89
  platform: "node",
83
90
  target: "node20",
84
91
  bundle: true,
85
- banner: {
86
- js: "import { createRequire } from 'module'; const require = createRequire(import.meta.url);",
87
- },
92
+ banner: { js: bannerJs },
88
93
  external: options.external,
89
- define,
90
94
  tsconfig: path.join(options.pkgDir, "tsconfig.json"),
91
95
  logLevel: "silent",
92
96
  };
@@ -82,10 +82,9 @@ export async function createClientViteConfig(
82
82
  : [options.browserslist]
83
83
  : undefined;
84
84
 
85
- // define: 환경변수 주입
85
+ // define: 환경변수 주입 (import.meta.env.KEY → Vite가 bare import.meta.env 객체를 자동 구성)
86
86
  const define: Record<string, string> = {};
87
87
  if (options.env != null) {
88
- define["process.env"] = JSON.stringify(options.env);
89
88
  for (const [key, value] of Object.entries(options.env)) {
90
89
  define[`import.meta.env.${key}`] = JSON.stringify(value);
91
90
  }
@@ -31,8 +31,13 @@ export function angularVitestPlugin(options: AngularVitestPluginOptions): Plugin
31
31
  (f) => f.includes("/src/") || f.includes(".fixture."),
32
32
  );
33
33
 
34
+ const angularCompilerOptions = configFile.config?.angularCompilerOptions as
35
+ | Record<string, unknown>
36
+ | undefined;
37
+
34
38
  const compilerOptions: ts.CompilerOptions = {
35
39
  ...parsedConfig.options,
40
+ ...(angularCompilerOptions as ts.CompilerOptions | undefined),
36
41
  noEmit: false,
37
42
  declaration: false,
38
43
  declarationMap: false,
@@ -1,8 +1,7 @@
1
1
  import path from "path";
2
2
  import fs from "fs";
3
- import { execaSync } from "execa";
4
3
  import esbuild from "esbuild";
5
- import { createWorker, FsWatcher, pathx } from "@simplysm/core-node";
4
+ import { cpx, createWorker, FsWatcher, pathx } from "@simplysm/core-node";
6
5
  import { err as errNs } from "@simplysm/core-common";
7
6
  import { consola } from "consola";
8
7
  import type { BuildOutput } from "../engines/types";
@@ -231,7 +230,7 @@ function generateProductionFiles(
231
230
  distPkgJson["dependencies"] = resolveLockedVersions(info.cwd, externals);
232
231
  }
233
232
  if (info.packageManager === "volta") {
234
- const nodeVersion = execaSync("node", ["-v"]).stdout.trim();
233
+ const nodeVersion = cpx.execSync("node", ["-v"]).stdout.trim();
235
234
  distPkgJson["volta"] = { node: nodeVersion };
236
235
  }
237
236
  fs.writeFileSync(path.join(distDir, "package.json"), JSON.stringify(distPkgJson, undefined, 2));
@@ -25,6 +25,10 @@ vi.mock("@simplysm/core-node", () => ({
25
25
  glob: mockFsxGlob,
26
26
  copy: mockFsxCopy,
27
27
  },
28
+ cpx: {
29
+ exec: mockCpxExec,
30
+ execSync: vi.fn().mockReturnValue({ stdout: "", stderr: "", exitCode: 0 }),
31
+ },
28
32
  }));
29
33
 
30
34
  // env mock
@@ -35,14 +39,12 @@ vi.mock("@simplysm/core-common", () => ({
35
39
  }),
36
40
  }));
37
41
 
38
- // execa mock
42
+ // cpx mock (was execa)
39
43
  const execaCalls: { command: string; args: string[] }[] = [];
40
- vi.mock("execa", () => ({
41
- execa: vi.fn((...args: unknown[]) => {
42
- execaCalls.push({ command: args[0] as string, args: (args[1] as string[] | undefined) ?? [] });
43
- return Promise.resolve({ stdout: "", stderr: "" });
44
- }),
45
- }));
44
+ const mockCpxExec = vi.fn((...args: unknown[]) => {
45
+ execaCalls.push({ command: args[0] as string, args: (args[1] as string[] | undefined) ?? [] });
46
+ return Promise.resolve({ stdout: "", stderr: "", exitCode: 0 });
47
+ });
46
48
 
47
49
  const mockFsWriteFile = vi.fn().mockResolvedValue(undefined);
48
50
  vi.mock("node:fs", () => ({
@@ -23,6 +23,10 @@ vi.mock("@simplysm/core-node", () => ({
23
23
  rm: mockFsxRm,
24
24
  glob: mockFsxGlob,
25
25
  },
26
+ cpx: {
27
+ exec: mockCpxExec,
28
+ execSync: vi.fn().mockReturnValue({ stdout: "", stderr: "", exitCode: 0 }),
29
+ },
26
30
  }));
27
31
 
28
32
  // env mock
@@ -33,14 +37,12 @@ vi.mock("@simplysm/core-common", () => ({
33
37
  }),
34
38
  }));
35
39
 
36
- // execa mock
40
+ // cpx mock (was execa)
37
41
  const execaCalls: { command: string; args: string[] }[] = [];
38
- vi.mock("execa", () => ({
39
- execa: vi.fn((...args: unknown[]) => {
40
- execaCalls.push({ command: args[0] as string, args: (args[1] as string[] | undefined) ?? [] });
41
- return Promise.resolve({ stdout: "", stderr: "" });
42
- }),
43
- }));
42
+ const mockCpxExec = vi.fn((...args: unknown[]) => {
43
+ execaCalls.push({ command: args[0] as string, args: (args[1] as string[] | undefined) ?? [] });
44
+ return Promise.resolve({ stdout: "", stderr: "", exitCode: 0 });
45
+ });
44
46
 
45
47
  const mockFsWriteFile = vi.fn().mockResolvedValue(undefined);
46
48
  vi.mock("node:fs", () => ({
@@ -24,6 +24,10 @@ vi.mock("@simplysm/core-node", () => ({
24
24
  glob: mockFsxGlob,
25
25
  copy: mockFsxCopy,
26
26
  },
27
+ cpx: {
28
+ exec: mockCpxExec,
29
+ execSync: vi.fn().mockReturnValue({ stdout: "", stderr: "", exitCode: 0 }),
30
+ },
27
31
  }));
28
32
 
29
33
  let mockEnv: Record<string, unknown> = {};
@@ -34,12 +38,10 @@ vi.mock("@simplysm/core-common", () => ({
34
38
  }));
35
39
 
36
40
  const execaCalls: { command: string; args: string[] }[] = [];
37
- vi.mock("execa", () => ({
38
- execa: vi.fn((...args: unknown[]) => {
39
- execaCalls.push({ command: args[0] as string, args: (args[1] as string[] | undefined) ?? [] });
40
- return Promise.resolve({ stdout: "", stderr: "" });
41
- }),
42
- }));
41
+ const mockCpxExec = vi.fn((...args: unknown[]) => {
42
+ execaCalls.push({ command: args[0] as string, args: (args[1] as string[] | undefined) ?? [] });
43
+ return Promise.resolve({ stdout: "", stderr: "", exitCode: 0 });
44
+ });
43
45
 
44
46
  const mockFsWriteFile = vi.fn().mockResolvedValue(undefined);
45
47
  vi.mock("node:fs", () => ({
@@ -25,6 +25,10 @@ vi.mock("@simplysm/core-node", () => ({
25
25
  glob: mockFsxGlob,
26
26
  copy: mockFsxCopy,
27
27
  },
28
+ cpx: {
29
+ exec: mockCpxExec,
30
+ execSync: vi.fn().mockReturnValue({ stdout: "", stderr: "", exitCode: 0 }),
31
+ },
28
32
  }));
29
33
 
30
34
  // env mock
@@ -35,17 +39,15 @@ vi.mock("@simplysm/core-common", () => ({
35
39
  }),
36
40
  }));
37
41
 
38
- // execa mock — tracks commands and resolves immediately
42
+ // cpx mock (was execa) — tracks commands and resolves immediately
39
43
  const execaCalls: { command: string; args: string[] }[] = [];
40
- let execaFactory: (...args: unknown[]) => Promise<{ stdout: string; stderr: string }> = () =>
41
- Promise.resolve({ stdout: "", stderr: "" });
44
+ let execaFactory: (...args: unknown[]) => Promise<{ stdout: string; stderr: string; exitCode: number }> = () =>
45
+ Promise.resolve({ stdout: "", stderr: "", exitCode: 0 });
42
46
 
43
- vi.mock("execa", () => ({
44
- execa: vi.fn((...args: unknown[]) => {
45
- execaCalls.push({ command: args[0] as string, args: (args[1] as string[] | undefined) ?? [] });
46
- return execaFactory(...args);
47
- }),
48
- }));
47
+ const mockCpxExec = vi.fn((...args: unknown[]) => {
48
+ execaCalls.push({ command: args[0] as string, args: (args[1] as string[] | undefined) ?? [] });
49
+ return execaFactory(...args);
50
+ });
49
51
 
50
52
  const mockFsWriteFile = vi.fn().mockResolvedValue(undefined);
51
53
  vi.mock("node:fs", () => ({
@@ -135,7 +137,7 @@ export default config;`;
135
137
  mockEnv = { ANDROID_HOME: "C:/Android/Sdk" };
136
138
 
137
139
  execaCalls.length = 0;
138
- execaFactory = () => Promise.resolve({ stdout: "", stderr: "" });
140
+ execaFactory = () => Promise.resolve({ stdout: "", stderr: "", exitCode: 0 });
139
141
  mockFsWriteFile.mockReset();
140
142
  mockFsWriteFile.mockResolvedValue(undefined);
141
143
  }
@@ -195,7 +197,7 @@ describe("Capacitor.run()", () => {
195
197
  return Promise.reject(new Error("cap run failed"));
196
198
  }
197
199
  }
198
- return Promise.resolve({ stdout: "", stderr: "" });
200
+ return Promise.resolve({ stdout: "", stderr: "", exitCode: 0 });
199
201
  };
200
202
 
201
203
  const cap = await Capacitor.create(PKG_PATH, {
@@ -24,6 +24,10 @@ vi.mock("@simplysm/core-node", () => ({
24
24
  glob: mockFsxGlob,
25
25
  copy: mockFsxCopy,
26
26
  },
27
+ cpx: {
28
+ exec: mockCpxExec,
29
+ execSync: vi.fn().mockReturnValue({ stdout: "", stderr: "", exitCode: 0 }),
30
+ },
27
31
  }));
28
32
 
29
33
  let mockEnv: Record<string, unknown> = {};
@@ -34,12 +38,10 @@ vi.mock("@simplysm/core-common", () => ({
34
38
  }));
35
39
 
36
40
  const execaCalls: { command: string; args: string[] }[] = [];
37
- vi.mock("execa", () => ({
38
- execa: vi.fn((...args: unknown[]) => {
39
- execaCalls.push({ command: args[0] as string, args: (args[1] as string[] | undefined) ?? [] });
40
- return Promise.resolve({ stdout: "", stderr: "" });
41
- }),
42
- }));
41
+ const mockCpxExec = vi.fn((...args: unknown[]) => {
42
+ execaCalls.push({ command: args[0] as string, args: (args[1] as string[] | undefined) ?? [] });
43
+ return Promise.resolve({ stdout: "", stderr: "", exitCode: 0 });
44
+ });
43
45
 
44
46
  const mockFsWriteFile = vi.fn().mockResolvedValue(undefined);
45
47
  vi.mock("node:fs", () => ({