@simplysm/sd-cli 13.0.93 → 13.0.96

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 (67) hide show
  1. package/README.md +67 -44
  2. package/dist/capacitor/capacitor.d.ts +15 -1
  3. package/dist/capacitor/capacitor.d.ts.map +1 -1
  4. package/dist/capacitor/capacitor.js +52 -31
  5. package/dist/capacitor/capacitor.js.map +1 -1
  6. package/dist/electron/electron.d.ts +6 -2
  7. package/dist/electron/electron.d.ts.map +1 -1
  8. package/dist/electron/electron.js +12 -6
  9. package/dist/electron/electron.js.map +1 -1
  10. package/dist/orchestrators/DevOrchestrator.d.ts.map +1 -1
  11. package/dist/orchestrators/DevOrchestrator.js +22 -4
  12. package/dist/orchestrators/DevOrchestrator.js.map +1 -1
  13. package/dist/sd-cli-entry.d.ts.map +1 -1
  14. package/dist/sd-cli-entry.js +4 -1
  15. package/dist/sd-cli-entry.js.map +1 -1
  16. package/dist/utils/esbuild-config.d.ts +2 -0
  17. package/dist/utils/esbuild-config.d.ts.map +1 -1
  18. package/dist/utils/esbuild-config.js +86 -6
  19. package/dist/utils/esbuild-config.js.map +1 -1
  20. package/dist/utils/package-utils.d.ts.map +1 -1
  21. package/dist/utils/package-utils.js +7 -0
  22. package/dist/utils/package-utils.js.map +1 -1
  23. package/dist/utils/replace-deps.d.ts.map +1 -1
  24. package/dist/utils/replace-deps.js +17 -0
  25. package/dist/utils/replace-deps.js.map +1 -1
  26. package/dist/utils/worker-utils.d.ts +9 -1
  27. package/dist/utils/worker-utils.d.ts.map +1 -1
  28. package/dist/utils/worker-utils.js +7 -0
  29. package/dist/utils/worker-utils.js.map +1 -1
  30. package/dist/workers/client.worker.d.ts.map +1 -1
  31. package/dist/workers/client.worker.js +2 -1
  32. package/dist/workers/client.worker.js.map +1 -1
  33. package/dist/workers/dts.worker.d.ts.map +1 -1
  34. package/dist/workers/dts.worker.js +2 -1
  35. package/dist/workers/dts.worker.js.map +1 -1
  36. package/dist/workers/library.worker.d.ts.map +1 -1
  37. package/dist/workers/library.worker.js +2 -1
  38. package/dist/workers/library.worker.js.map +1 -1
  39. package/dist/workers/server-runtime.worker.d.ts +1 -0
  40. package/dist/workers/server-runtime.worker.d.ts.map +1 -1
  41. package/dist/workers/server-runtime.worker.js +36 -2
  42. package/dist/workers/server-runtime.worker.js.map +1 -1
  43. package/dist/workers/server.worker.d.ts.map +1 -1
  44. package/dist/workers/server.worker.js +144 -4
  45. package/dist/workers/server.worker.js.map +1 -1
  46. package/docs/architecture.md +14 -14
  47. package/docs/config-types.md +12 -2
  48. package/package.json +4 -4
  49. package/src/capacitor/capacitor.ts +59 -31
  50. package/src/electron/electron.ts +13 -6
  51. package/src/orchestrators/DevOrchestrator.ts +20 -1
  52. package/src/sd-cli-entry.ts +4 -1
  53. package/src/utils/esbuild-config.ts +86 -6
  54. package/src/utils/package-utils.ts +8 -0
  55. package/src/utils/replace-deps.ts +20 -0
  56. package/src/utils/worker-utils.ts +14 -1
  57. package/src/workers/client.worker.ts +3 -1
  58. package/src/workers/dts.worker.ts +3 -1
  59. package/src/workers/library.worker.ts +3 -1
  60. package/src/workers/server-runtime.worker.ts +42 -2
  61. package/src/workers/server.worker.ts +165 -3
  62. package/templates/init/package.json.hbs +3 -3
  63. package/templates/init/packages/client-admin/package.json.hbs +7 -7
  64. package/templates/init/packages/db-main/package.json.hbs +2 -2
  65. package/templates/init/packages/server/package.json.hbs +5 -5
  66. package/templates/init/tests-e2e/package.json.hbs +1 -1
  67. package/tests/capacitor.spec.ts +49 -0
@@ -39,6 +39,8 @@ export class Capacitor {
39
39
  private static readonly _ANDROID_KEYSTORE_FILE_NAME = "android.keystore";
40
40
  private static readonly _LOCK_FILE_NAME = ".capacitor.lock";
41
41
  private static readonly _logger = consola.withTag("sd:cli:capacitor");
42
+ private static readonly _utf8Decoder = new TextDecoder("utf-8", { fatal: true });
43
+ private static readonly _fallbackDecoder = new TextDecoder("euc-kr");
42
44
 
43
45
  private readonly _capPath: string;
44
46
  private readonly _platforms: string[];
@@ -78,7 +80,7 @@ export class Capacitor {
78
80
  if (typeof config.appName !== "string" || config.appName.trim() === "") {
79
81
  throw new CapacitorConfigError("capacitor.appName is required.");
80
82
  }
81
- if (!/^[a-zA-Z0-9 \-]+$/.test(config.appName)) {
83
+ if (!/^[\p{L}\p{N} \-]+$/u.test(config.appName)) {
82
84
  throw new CapacitorConfigError(`capacitor.appName contains invalid characters: ${config.appName}`);
83
85
  }
84
86
  if (config.platform != null) {
@@ -91,14 +93,47 @@ export class Capacitor {
91
93
  }
92
94
  }
93
95
 
96
+ /**
97
+ * Execute a Capacitor CLI command via npx
98
+ */
99
+ private async _execCap(args: string[]): Promise<string> {
100
+ return this._exec("npx", ["cap", ...args], this._capPath);
101
+ }
102
+
103
+ /**
104
+ * Execute capacitor-assets CLI command via npx
105
+ */
106
+ private async _execCapAssets(args: string[]): Promise<string> {
107
+ return this._exec("npx", ["capacitor-assets", ...args], this._capPath);
108
+ }
109
+
94
110
  /**
95
111
  * Execute command (with logging)
96
112
  */
97
113
  private async _exec(cmd: string, args: string[], cwd: string): Promise<string> {
98
114
  Capacitor._logger.debug(`executed command: ${cmd} ${args.join(" ")}`);
99
- const { stdout: result } = await execa(cmd, args, { cwd });
100
- Capacitor._logger.debug(`execution result: ${result}`);
101
- return result;
115
+
116
+ const result = await execa(cmd, args, { cwd, reject: false, encoding: "buffer" });
117
+ const stdout = Capacitor._decodeOutput(result.stdout);
118
+
119
+ if (result.exitCode !== 0) {
120
+ const stderr = Capacitor._decodeOutput(result.stderr);
121
+ throw new Error(`${cmd} ${args.join(" ")} failed (exit ${result.exitCode}): ${stderr || stdout}`);
122
+ }
123
+
124
+ Capacitor._logger.debug(`execution result: ${stdout}`);
125
+ return stdout;
126
+ }
127
+
128
+ /**
129
+ * Decode command output (UTF-8 first, fallback to EUC-KR for Windows CP949)
130
+ */
131
+ private static _decodeOutput(bytes: Uint8Array): string {
132
+ try {
133
+ return Capacitor._utf8Decoder.decode(bytes);
134
+ } catch {
135
+ return Capacitor._fallbackDecoder.decode(bytes);
136
+ }
102
137
  }
103
138
 
104
139
  /**
@@ -186,9 +221,9 @@ export class Capacitor {
186
221
 
187
222
  // 6. Synchronize web assets
188
223
  if (changed) {
189
- await this._exec("npx", ["cap", "sync"], this._capPath);
224
+ await this._execCap(["sync"]);
190
225
  } else {
191
- await this._exec("npx", ["cap", "copy"], this._capPath);
226
+ await this._execCap(["copy"]);
192
227
  }
193
228
  } finally {
194
229
  await this._releaseLock();
@@ -205,7 +240,7 @@ export class Capacitor {
205
240
  const buildType = this._config.debug ? "debug" : "release";
206
241
 
207
242
  for (const platform of this._platforms) {
208
- await this._exec("npx", ["cap", "copy", platform], this._capPath);
243
+ await this._execCap(["copy", platform]);
209
244
 
210
245
  if (platform === "android") {
211
246
  await this._buildAndroid(outPath, buildType);
@@ -229,10 +264,10 @@ export class Capacitor {
229
264
  }
230
265
 
231
266
  for (const platform of this._platforms) {
232
- await this._exec("npx", ["cap", "copy", platform], this._capPath);
267
+ await this._execCap(["copy", platform]);
233
268
 
234
269
  try {
235
- await this._exec("npx", ["cap", "run", platform], this._capPath);
270
+ await this._execCap(["run", platform]);
236
271
  } catch (err) {
237
272
  if (platform === "android") {
238
273
  try {
@@ -266,24 +301,22 @@ export class Capacitor {
266
301
  //#region Private - Initialization
267
302
 
268
303
  /**
269
- * Basic Capacitor project initialization (package.json, npm install, cap init)
304
+ * Basic Capacitor project initialization (package.json, pnpm install, cap init)
270
305
  */
271
306
  private async _initCap(): Promise<boolean> {
272
307
  const depChanged = await this._setupNpmConf();
273
- if (!depChanged) return false;
308
+ const nodeModulesExists = await fsx.exists(path.resolve(this._capPath, "node_modules"));
309
+
310
+ if (!depChanged && nodeModulesExists) return false;
274
311
 
275
312
  // pnpm install
276
- const installResult = await this._exec("pnpm", ["install"], this._capPath);
277
- Capacitor._logger.debug(`pnpm install completed: ${installResult}`);
313
+ const installResult = await this._exec("npm", ["install"], this._capPath);
314
+ Capacitor._logger.debug(`npm install completed: ${installResult}`);
278
315
 
279
316
  // F12: cap init idempotency - execute only when capacitor.config.ts does not exist
280
317
  const configPath = path.resolve(this._capPath, "capacitor.config.ts");
281
318
  if (!(await fsx.exists(configPath))) {
282
- await this._exec(
283
- "npx",
284
- ["cap", "init", this._config.appName, this._config.appId],
285
- this._capPath,
286
- );
319
+ await this._execCap(["init", this._config.appId, this._config.appId]);
287
320
  }
288
321
 
289
322
  // Create default www/index.html
@@ -430,7 +463,7 @@ export default config;
430
463
  continue;
431
464
  }
432
465
 
433
- await this._exec("npx", ["cap", "add", platform], this._capPath);
466
+ await this._execCap(["add", platform]);
434
467
  }
435
468
  }
436
469
 
@@ -475,18 +508,13 @@ export default config;
475
508
  })
476
509
  .toFile(logoPath);
477
510
 
478
- await this._exec(
479
- "npx",
480
- [
481
- "@capacitor/assets",
482
- "generate",
483
- "--iconBackgroundColor",
484
- "#ffffff",
485
- "--splashBackgroundColor",
486
- "#ffffff",
487
- ],
488
- this._capPath,
489
- );
511
+ await this._execCapAssets([
512
+ "generate",
513
+ "--iconBackgroundColor",
514
+ "#ffffff",
515
+ "--splashBackgroundColor",
516
+ "#ffffff",
517
+ ]);
490
518
  } catch (err) {
491
519
  Capacitor._logger.warn(
492
520
  `icon generation failed: ${err instanceof Error ? err.message : err}. Using default icon.`,
@@ -58,6 +58,13 @@ export class Electron {
58
58
  }
59
59
  }
60
60
 
61
+ /**
62
+ * Resolve binary path from {pkgPath}/node_modules/.bin/
63
+ */
64
+ private _localBin(name: string): string {
65
+ return path.resolve(this._pkgPath, "node_modules/.bin", name);
66
+ }
67
+
61
68
  /**
62
69
  * Execute command (with logging)
63
70
  */
@@ -79,7 +86,7 @@ export class Electron {
79
86
  * Initialize Electron project
80
87
  *
81
88
  * 1. Create .electron/src/package.json
82
- * 2. Run npm install
89
+ * 2. Run pnpm install
83
90
  * 3. Run electron-rebuild (rebuild native modules)
84
91
  */
85
92
  async initialize(): Promise<void> {
@@ -94,7 +101,7 @@ export class Electron {
94
101
  // 3. Rebuild native modules
95
102
  const reinstallDeps = this._config.reinstallDependencies ?? [];
96
103
  if (reinstallDeps.length > 0) {
97
- await this._exec("npx", ["electron-rebuild"], srcPath);
104
+ await this._exec(this._localBin("electron-rebuild"), [], srcPath);
98
105
  }
99
106
  }
100
107
 
@@ -128,7 +135,7 @@ export class Electron {
128
135
  *
129
136
  * 1. Bundle electron-main.ts with esbuild
130
137
  * 2. Create dist/electron/package.json
131
- * 3. Run npx electron .
138
+ * 3. Run electron .
132
139
  */
133
140
  async run(url?: string): Promise<void> {
134
141
  const electronRunPath = path.resolve(this._pkgPath, "dist/electron");
@@ -154,7 +161,7 @@ export class Electron {
154
161
  runEnv["ELECTRON_DEV_URL"] = url;
155
162
  }
156
163
 
157
- await this._exec("npx", ["electron", "."], electronRunPath, runEnv);
164
+ await this._exec(this._localBin("electron"), ["."], electronRunPath, runEnv);
158
165
  }
159
166
 
160
167
  //#endregion
@@ -300,8 +307,8 @@ export class Electron {
300
307
  await fsx.writeJson(configFilePath, builderConfig, { space: 2 });
301
308
 
302
309
  await this._exec(
303
- "npx",
304
- ["electron-builder", "--win", "--config", configFilePath],
310
+ this._localBin("electron-builder"),
311
+ ["--win", "--config", configFilePath],
305
312
  this._pkgPath,
306
313
  );
307
314
  }
@@ -498,7 +498,7 @@ export class DevOrchestrator {
498
498
  };
499
499
 
500
500
  // Register Server Build Worker event handlers
501
- for (const { name } of this._serverPackages) {
501
+ for (const { name, config } of this._serverPackages) {
502
502
  const serverBuild = this._serverBuildWorkers.get(name)!;
503
503
 
504
504
  serverBuild.worker.on("buildStart", () => {
@@ -540,6 +540,7 @@ export class DevOrchestrator {
540
540
  serverRuntimePromises,
541
541
  resolveServerStep,
542
542
  viteClientReadyPromises,
543
+ { ...this._baseEnv, ...config.env },
543
544
  ).catch((err: unknown) => {
544
545
  const message = errNs.message(err);
545
546
  this._logger.error(`[${name}] Error starting Server Runtime:`, message);
@@ -610,7 +611,9 @@ export class DevOrchestrator {
610
611
  serverRuntimePromises: Map<string, { promise: Promise<void>; resolver: () => void }>,
611
612
  resolveServerStep: (serverName: string, resultKey: string, result: BuildResult) => void,
612
613
  viteClientReadyPromises: Map<string, { promise: Promise<void>; resolver: () => void }>,
614
+ env?: Record<string, string>,
613
615
  ): Promise<void> {
616
+ const runtimeStartTime = performance.now();
614
617
  this._logger.debug(`[${serverName}] _startServerRuntime: ${mainJsPath}`);
615
618
  const updatedBuild = this._serverBuildWorkers.get(serverName)!;
616
619
  updatedBuild.mainJsPath = mainJsPath;
@@ -619,10 +622,15 @@ export class DevOrchestrator {
619
622
  const existingRuntime = this._serverRuntimeWorkers.get(serverName);
620
623
  if (existingRuntime != null) {
621
624
  this._logger.info(`[${serverName}] Restarting server...`);
625
+ const terminateStart = performance.now();
622
626
  await existingRuntime.terminate();
627
+ this._logger.debug(
628
+ `[${serverName}] Previous runtime terminated (${Math.round(performance.now() - terminateStart)}ms)`,
629
+ );
623
630
  }
624
631
 
625
632
  // Create and start new Server Runtime Worker
633
+ this._logger.debug(`[${serverName}] Creating runtime worker...`);
626
634
  const runtimeWorker = Worker.create<typeof ServerRuntimeWorkerModule>(serverRuntimeWorkerPath);
627
635
  this._serverRuntimeWorkers.set(serverName, runtimeWorker);
628
636
 
@@ -635,7 +643,11 @@ export class DevOrchestrator {
635
643
  `[${serverName}] Waiting for clients: ${String(clientReadyPromises.length)} total`,
636
644
  );
637
645
  if (clientReadyPromises.length > 0) {
646
+ const waitStart = performance.now();
638
647
  await Promise.all(clientReadyPromises);
648
+ this._logger.debug(
649
+ `[${serverName}] Clients ready (${Math.round(performance.now() - waitStart)}ms)`,
650
+ );
639
651
  }
640
652
 
641
653
  // Collect client ports for this server
@@ -645,6 +657,9 @@ export class DevOrchestrator {
645
657
  serverClientPorts[clientName] = this._clientPorts[clientName];
646
658
  }
647
659
  }
660
+ this._logger.debug(
661
+ `[${serverName}] Client ports: ${JSON.stringify(serverClientPorts)}`,
662
+ );
648
663
 
649
664
  // Server Runtime event handlers
650
665
  runtimeWorker.on("serverReady", (readyData) => {
@@ -672,10 +687,14 @@ export class DevOrchestrator {
672
687
  // Start Server Runtime
673
688
  // If worker crashes, it terminates without emitting "serverReady"/"error" events,
674
689
  // so catch promise rejection to prevent hanging
690
+ this._logger.debug(
691
+ `[${serverName}] Starting runtime worker... (setup took ${Math.round(performance.now() - runtimeStartTime)}ms)`,
692
+ );
675
693
  runtimeWorker
676
694
  .start({
677
695
  mainJsPath,
678
696
  clientPorts: serverClientPorts,
697
+ env,
679
698
  })
680
699
  .catch((err: unknown) => {
681
700
  const message = errNs.message(err);
@@ -77,7 +77,10 @@ export function createCliParser(argv: string[]): Argv {
77
77
  global: true,
78
78
  })
79
79
  .middleware((args) => {
80
- if (args.debug) consola.level = LogLevels.debug;
80
+ if (args.debug) {
81
+ consola.level = LogLevels.debug;
82
+ process.env["SD_DEBUG"] = "true";
83
+ }
81
84
  })
82
85
  .command(
83
86
  "lint [targets..]",
@@ -5,6 +5,9 @@ import { createRequire } from "module";
5
5
  import type esbuild from "esbuild";
6
6
  import { solidPlugin } from "esbuild-plugin-solid";
7
7
  import type { TypecheckEnv } from "./tsconfig";
8
+ import { consola } from "consola";
9
+
10
+ const logger = consola.withTag("sd:cli:esbuild-config");
8
11
 
9
12
  /**
10
13
  * Write only changed files from esbuild outputFiles to disk
@@ -65,6 +68,8 @@ export interface ServerEsbuildOptions {
65
68
  env?: Record<string, string>;
66
69
  /** External modules to exclude from bundle */
67
70
  external?: string[];
71
+ /** Dev mode: skip minification for faster builds */
72
+ dev?: boolean;
68
73
  }
69
74
 
70
75
  /**
@@ -102,7 +107,7 @@ export function createLibraryEsbuildOptions(options: LibraryEsbuildOptions): esb
102
107
  bundle: false,
103
108
  write: false,
104
109
  tsconfigRaw: {
105
- compilerOptions: options.compilerOptions as esbuild.TsconfigRaw["compilerOptions"],
110
+ compilerOptions: toEsbuildTsconfigRaw(options.compilerOptions),
106
111
  },
107
112
  logLevel: "silent",
108
113
  plugins,
@@ -130,7 +135,7 @@ export function createServerEsbuildOptions(options: ServerEsbuildOptions): esbui
130
135
  entryPoints: options.entryPoints,
131
136
  outdir: path.join(options.pkgDir, "dist"),
132
137
  format: "esm",
133
- minify: true,
138
+ minify: options.dev !== true,
134
139
  platform: "node",
135
140
  target: "node20",
136
141
  bundle: true,
@@ -140,12 +145,64 @@ export function createServerEsbuildOptions(options: ServerEsbuildOptions): esbui
140
145
  external: options.external,
141
146
  define,
142
147
  tsconfigRaw: {
143
- compilerOptions: options.compilerOptions as esbuild.TsconfigRaw["compilerOptions"],
148
+ compilerOptions: toEsbuildTsconfigRaw(options.compilerOptions),
144
149
  },
145
150
  logLevel: "silent",
146
151
  };
147
152
  }
148
153
 
154
+ // TypeScript ScriptTarget enum → esbuild target string
155
+ const TARGET_MAP: Record<number, string> = {
156
+ 0: "es3", 1: "es5", 2: "es2015", 3: "es2016", 4: "es2017",
157
+ 5: "es2018", 6: "es2019", 7: "es2020", 8: "es2021", 9: "es2022",
158
+ 10: "es2023", 11: "es2024", 99: "esnext",
159
+ };
160
+
161
+ // TypeScript JsxEmit enum → esbuild jsx string
162
+ const JSX_MAP: Record<number, string> = {
163
+ 1: "preserve", 2: "react", 3: "react-native", 4: "react-jsx", 5: "react-jsxdev",
164
+ };
165
+
166
+ // TypeScript ModuleKind enum → string
167
+ const MODULE_MAP: Record<number, string> = {
168
+ 0: "none", 1: "commonjs", 2: "amd", 3: "umd", 4: "system",
169
+ 5: "es2015", 6: "es2020", 7: "es2022", 99: "esnext",
170
+ 100: "node16", 199: "nodenext",
171
+ };
172
+
173
+ // TypeScript ModuleResolutionKind enum → string
174
+ const MODULE_RESOLUTION_MAP: Record<number, string> = {
175
+ 1: "node10", 2: "node16", 3: "nodenext", 100: "bundler",
176
+ };
177
+
178
+ /**
179
+ * Convert TypeScript's ts.CompilerOptions to esbuild-compatible tsconfigRaw compilerOptions.
180
+ *
181
+ * TypeScript uses numeric enum values (e.g., target: 99) while esbuild expects
182
+ * string values (e.g., "esnext"). This converts known enum fields and passes
183
+ * everything else through as-is.
184
+ */
185
+ function toEsbuildTsconfigRaw(
186
+ compilerOptions: Record<string, unknown>,
187
+ ): esbuild.TsconfigRaw["compilerOptions"] {
188
+ const result = { ...compilerOptions };
189
+
190
+ if (typeof result["target"] === "number") {
191
+ result["target"] = TARGET_MAP[result["target"]] ?? "esnext";
192
+ }
193
+ if (typeof result["jsx"] === "number") {
194
+ result["jsx"] = JSX_MAP[result["jsx"]];
195
+ }
196
+ if (typeof result["module"] === "number") {
197
+ result["module"] = MODULE_MAP[result["module"]];
198
+ }
199
+ if (typeof result["moduleResolution"] === "number") {
200
+ result["moduleResolution"] = MODULE_RESOLUTION_MAP[result["moduleResolution"]];
201
+ }
202
+
203
+ return result as esbuild.TsconfigRaw["compilerOptions"];
204
+ }
205
+
149
206
  /**
150
207
  * Extract TypecheckEnv from build target
151
208
  *
@@ -183,6 +240,7 @@ function scanDependencyTree(
183
240
  try {
184
241
  pkgJsonPath = req.resolve(`${pkgName}/package.json`);
185
242
  } catch {
243
+ logger.debug(`[scanDependencyTree] Could not resolve: ${pkgName}`);
186
244
  return;
187
245
  }
188
246
 
@@ -196,7 +254,13 @@ function scanDependencyTree(
196
254
  }
197
255
 
198
256
  // Recursively traverse sub-dependencies
199
- for (const dep of Object.keys(pkgJson.dependencies ?? {})) {
257
+ const subDeps = Object.keys(pkgJson.dependencies ?? {});
258
+ if (subDeps.length > 0) {
259
+ logger.debug(
260
+ `[scanDependencyTree] ${pkgName}: traversing ${String(subDeps.length)} sub-dependencies`,
261
+ );
262
+ }
263
+ for (const dep of subDeps) {
200
264
  scanDependencyTree(dep, depDir, external, visited, collector);
201
265
  }
202
266
  }
@@ -212,7 +276,12 @@ export function collectUninstalledOptionalPeerDeps(pkgDir: string): string[] {
212
276
  const visited = new Set<string>();
213
277
 
214
278
  const pkgJson = JSON.parse(readFileSync(path.join(pkgDir, "package.json"), "utf-8")) as PkgJson;
215
- for (const dep of Object.keys(pkgJson.dependencies ?? {})) {
279
+ const deps = Object.keys(pkgJson.dependencies ?? {});
280
+ logger.debug(
281
+ `[optionalPeerDeps] Scanning ${String(deps.length)} top-level dependencies...`,
282
+ );
283
+
284
+ for (const dep of deps) {
216
285
  scanDependencyTree(dep, pkgDir, external, visited, (_pkgName, depDir, depPkgJson) => {
217
286
  const found: string[] = [];
218
287
  if (depPkgJson.peerDependenciesMeta != null) {
@@ -232,6 +301,9 @@ export function collectUninstalledOptionalPeerDeps(pkgDir: string): string[] {
232
301
  });
233
302
  }
234
303
 
304
+ logger.debug(
305
+ `[optionalPeerDeps] Done: visited ${String(visited.size)} packages, found ${String(external.size)} externals`,
306
+ );
235
307
  return [...external];
236
308
  }
237
309
 
@@ -250,7 +322,12 @@ export function collectNativeModuleExternals(pkgDir: string): string[] {
250
322
  const visited = new Set<string>();
251
323
 
252
324
  const pkgJson = JSON.parse(readFileSync(path.join(pkgDir, "package.json"), "utf-8")) as PkgJson;
253
- for (const dep of Object.keys(pkgJson.dependencies ?? {})) {
325
+ const deps = Object.keys(pkgJson.dependencies ?? {});
326
+ logger.debug(
327
+ `[nativeModules] Scanning ${String(deps.length)} top-level dependencies...`,
328
+ );
329
+
330
+ for (const dep of deps) {
254
331
  scanDependencyTree(dep, pkgDir, external, visited, (pkgName, depDir, _pkgJson) => {
255
332
  const found: string[] = [];
256
333
  // Detect native modules by checking for binding.gyp
@@ -261,6 +338,9 @@ export function collectNativeModuleExternals(pkgDir: string): string[] {
261
338
  });
262
339
  }
263
340
 
341
+ logger.debug(
342
+ `[nativeModules] Done: visited ${String(visited.size)} packages, found ${String(external.size)} externals`,
343
+ );
264
344
  return [...external];
265
345
  }
266
346
 
@@ -1,7 +1,10 @@
1
1
  import path from "path";
2
2
  import fs from "fs";
3
+ import { consola } from "consola";
3
4
  import type { SdPackageConfig } from "../sd-config.types";
4
5
 
6
+ const logger = consola.withTag("sd:cli:package-utils");
7
+
5
8
  /**
6
9
  * Walk up from import.meta.dirname to find package.json and return package root
7
10
  */
@@ -25,6 +28,8 @@ export function collectDeps(
25
28
  cwd: string,
26
29
  replaceDepsConfig?: Record<string, string>,
27
30
  ): DepsResult {
31
+ const startTime = performance.now();
32
+ logger.debug("[collectDeps] Starting dependency collection...");
28
33
  const rootPkgJsonPath = path.join(cwd, "package.json");
29
34
  const rootPkgJson = JSON.parse(fs.readFileSync(rootPkgJsonPath, "utf-8")) as { name: string };
30
35
  const scopeMatch = rootPkgJson.name.match(/^(@[^/]+)\//);
@@ -80,6 +85,9 @@ export function collectDeps(
80
85
  }
81
86
 
82
87
  traverse(pkgDir);
88
+ logger.debug(
89
+ `[collectDeps] Done: workspace=${String(workspaceDeps.length)}, replace=${String(replaceDeps.length)} (${Math.round(performance.now() - startTime)}ms)`,
90
+ );
83
91
  return { workspaceDeps, replaceDeps };
84
92
  }
85
93
 
@@ -3,6 +3,8 @@ import path from "path";
3
3
  import { glob } from "glob";
4
4
  import { consola } from "consola";
5
5
  import { fsx, pathx, FsWatcher } from "@simplysm/core-node";
6
+ import { exec } from "child_process";
7
+ import { promisify } from "util";
6
8
 
7
9
  /**
8
10
  * Match glob patterns from replaceDeps config with target package list
@@ -258,6 +260,24 @@ export async function setupReplaceDeps(
258
260
  }
259
261
 
260
262
  logger.success(`Replaced ${setupCount} dependencies`);
263
+
264
+ // Run postinstall scripts from replaced packages
265
+ for (const { targetName, resolvedSourcePath, actualTargetPath } of entries) {
266
+ const sourcePkgJsonPath = path.join(resolvedSourcePath, "package.json");
267
+ try {
268
+ const pkgJson = JSON.parse(await fs.promises.readFile(sourcePkgJsonPath, "utf-8"));
269
+ const postinstall = pkgJson.scripts?.postinstall as string | undefined;
270
+ if (postinstall == null) continue;
271
+
272
+ logger.start(`Running postinstall for ${targetName}`);
273
+ await promisify(exec)(postinstall, { cwd: actualTargetPath });
274
+ logger.success(`postinstall done: ${targetName}`);
275
+ } catch (err) {
276
+ logger.error(
277
+ `postinstall failed (${targetName}): ${err instanceof Error ? err.message : err}`,
278
+ );
279
+ }
280
+ }
261
281
  }
262
282
 
263
283
  /**
@@ -1,4 +1,17 @@
1
- import type { ConsolaInstance } from "consola";
1
+ import consola, { type ConsolaInstance, LogLevels } from "consola";
2
+
3
+ /**
4
+ * Apply debug log level in worker threads
5
+ *
6
+ * Checks the SD_DEBUG environment variable (set by --debug flag in main process)
7
+ * and applies debug log level to consola in the current worker thread.
8
+ * Must be called at worker module top level.
9
+ */
10
+ export function applyDebugLevel(): void {
11
+ if (process.env["SD_DEBUG"] === "true") {
12
+ consola.level = LogLevels.debug;
13
+ }
14
+ }
2
15
 
3
16
  /**
4
17
  * Register cleanup handlers for worker process shutdown signals
@@ -8,7 +8,9 @@ import type { SdClientPackageConfig } from "../sd-config.types";
8
8
  import { parseRootTsconfig, getCompilerOptionsForPackage } from "../utils/tsconfig";
9
9
  import { createViteConfig } from "../utils/vite-config";
10
10
  import { collectDeps } from "../utils/package-utils";
11
- import { registerCleanupHandlers, createOnceGuard } from "../utils/worker-utils";
11
+ import { registerCleanupHandlers, createOnceGuard, applyDebugLevel } from "../utils/worker-utils";
12
+
13
+ applyDebugLevel();
12
14
 
13
15
  //#region Types
14
16
 
@@ -11,7 +11,9 @@ import {
11
11
  type TypecheckEnv,
12
12
  } from "../utils/tsconfig";
13
13
  import { serializeDiagnostic, type SerializedDiagnostic } from "../utils/typecheck-serialization";
14
- import { createOnceGuard, registerCleanupHandlers } from "../utils/worker-utils";
14
+ import { createOnceGuard, registerCleanupHandlers, applyDebugLevel } from "../utils/worker-utils";
15
+
16
+ applyDebugLevel();
15
17
 
16
18
  //#region Types
17
19
 
@@ -14,7 +14,9 @@ import {
14
14
  getTypecheckEnvFromTarget,
15
15
  writeChangedOutputFiles,
16
16
  } from "../utils/esbuild-config";
17
- import { registerCleanupHandlers, createOnceGuard } from "../utils/worker-utils";
17
+ import { registerCleanupHandlers, createOnceGuard, applyDebugLevel } from "../utils/worker-utils";
18
+
19
+ applyDebugLevel();
18
20
 
19
21
  //#region Types
20
22