@simplysm/sd-cli 13.0.93 → 13.0.95

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 (64) hide show
  1. package/README.md +1 -1
  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 +16 -0
  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.map +1 -1
  40. package/dist/workers/server-runtime.worker.js +31 -2
  41. package/dist/workers/server-runtime.worker.js.map +1 -1
  42. package/dist/workers/server.worker.d.ts.map +1 -1
  43. package/dist/workers/server.worker.js +144 -4
  44. package/dist/workers/server.worker.js.map +1 -1
  45. package/package.json +4 -4
  46. package/src/capacitor/capacitor.ts +59 -31
  47. package/src/electron/electron.ts +13 -6
  48. package/src/orchestrators/DevOrchestrator.ts +16 -0
  49. package/src/sd-cli-entry.ts +4 -1
  50. package/src/utils/esbuild-config.ts +86 -6
  51. package/src/utils/package-utils.ts +8 -0
  52. package/src/utils/replace-deps.ts +20 -0
  53. package/src/utils/worker-utils.ts +14 -1
  54. package/src/workers/client.worker.ts +3 -1
  55. package/src/workers/dts.worker.ts +3 -1
  56. package/src/workers/library.worker.ts +3 -1
  57. package/src/workers/server-runtime.worker.ts +34 -2
  58. package/src/workers/server.worker.ts +165 -3
  59. package/templates/init/package.json.hbs +3 -3
  60. package/templates/init/packages/client-admin/package.json.hbs +7 -7
  61. package/templates/init/packages/db-main/package.json.hbs +2 -2
  62. package/templates/init/packages/server/package.json.hbs +5 -5
  63. package/templates/init/tests-e2e/package.json.hbs +1 -1
  64. package/tests/capacitor.spec.ts +49 -0
@@ -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
 
@@ -4,7 +4,7 @@ import { err as errNs } from "@simplysm/core-common";
4
4
  import { consola } from "consola";
5
5
  import net from "net";
6
6
  import { pathToFileURL } from "url";
7
- import { registerCleanupHandlers } from "../utils/worker-utils";
7
+ import { registerCleanupHandlers, applyDebugLevel } from "../utils/worker-utils";
8
8
 
9
9
  //#region Types
10
10
 
@@ -40,6 +40,8 @@ export interface ServerRuntimeWorkerEvents extends Record<string, unknown> {
40
40
 
41
41
  //#endregion
42
42
 
43
+ applyDebugLevel();
44
+
43
45
  const logger = consola.withTag("sd:cli:server-runtime:worker");
44
46
 
45
47
  /** Server instance (to be cleaned up) */
@@ -109,8 +111,13 @@ async function findAvailablePort(startPort: number, maxRetries = 20): Promise<nu
109
111
  */
110
112
  async function start(info: ServerRuntimeStartInfo): Promise<void> {
111
113
  try {
114
+ const startTime = performance.now();
115
+
112
116
  // Import main.js (must export a server instance)
117
+ logger.debug("[start] Importing main.js...");
118
+ let stepStart = performance.now();
113
119
  const module = await import(pathToFileURL(info.mainJsPath).href);
120
+ logger.debug(`[start] main.js imported (${Math.round(performance.now() - stepStart)}ms)`);
114
121
  const server = module.server;
115
122
 
116
123
  if (server == null) {
@@ -121,15 +128,28 @@ async function start(info: ServerRuntimeStartInfo): Promise<void> {
121
128
  serverInstance = server;
122
129
 
123
130
  // Find available port (auto-increment on port conflict)
131
+ logger.debug("[start] Finding available port...");
132
+ stepStart = performance.now();
124
133
  const originalPort = server.options.port;
125
134
  const availablePort = await findAvailablePort(originalPort);
126
135
  if (availablePort !== originalPort) {
127
136
  logger.info(`Port ${originalPort} in use, changing to ${availablePort}`);
128
137
  server.options.port = availablePort;
129
138
  }
139
+ logger.debug(
140
+ `[start] Port ${String(availablePort)} available (${Math.round(performance.now() - stepStart)}ms)`,
141
+ );
130
142
 
131
143
  // Configure Vite proxy (only if clientPorts exists)
132
- for (const [name, port] of Object.entries(info.clientPorts)) {
144
+ const clientEntries = Object.entries(info.clientPorts);
145
+ if (clientEntries.length > 0) {
146
+ logger.debug(
147
+ `[start] Configuring ${String(clientEntries.length)} Vite proxy(s)...`,
148
+ );
149
+ stepStart = performance.now();
150
+ }
151
+ for (const [name, port] of clientEntries) {
152
+ logger.debug(`[start] Registering proxy: /${name} -> http://127.0.0.1:${String(port)}`);
133
153
  await server.fastify.register(proxy, {
134
154
  prefix: `/${name}`,
135
155
  upstream: `http://127.0.0.1:${port}`,
@@ -137,9 +157,21 @@ async function start(info: ServerRuntimeStartInfo): Promise<void> {
137
157
  websocket: true,
138
158
  });
139
159
  }
160
+ if (clientEntries.length > 0) {
161
+ logger.debug(
162
+ `[start] Proxies configured (${Math.round(performance.now() - stepStart)}ms)`,
163
+ );
164
+ }
140
165
 
141
166
  // Start server
167
+ logger.debug("[start] Starting server listen...");
168
+ stepStart = performance.now();
142
169
  await server.listen();
170
+ logger.debug(`[start] Server listening (${Math.round(performance.now() - stepStart)}ms)`);
171
+
172
+ logger.debug(
173
+ `[start] Total runtime startup: ${Math.round(performance.now() - startTime)}ms`,
174
+ );
143
175
 
144
176
  sender.send("serverReady", { port: server.options.port });
145
177
  } catch (err) {
@@ -16,7 +16,7 @@ import {
16
16
  collectNativeModuleExternals,
17
17
  writeChangedOutputFiles,
18
18
  } from "../utils/esbuild-config";
19
- import { registerCleanupHandlers, createOnceGuard } from "../utils/worker-utils";
19
+ import { registerCleanupHandlers, createOnceGuard, applyDebugLevel } from "../utils/worker-utils";
20
20
  import { collectDeps } from "../utils/package-utils";
21
21
  import { copyPublicFiles, watchPublicFiles } from "../utils/copy-public";
22
22
 
@@ -101,6 +101,8 @@ export interface ServerWorkerEvents extends Record<string, unknown> {
101
101
 
102
102
  //#region Resource Management
103
103
 
104
+ applyDebugLevel();
105
+
104
106
  const logger = consola.withTag("sd:cli:server:worker");
105
107
 
106
108
  /** esbuild build context (to be cleaned up) */
@@ -151,8 +153,20 @@ async function cleanup(): Promise<void> {
151
153
  * 3. Manually specified in sd.config.ts
152
154
  */
153
155
  function collectAllExternals(pkgDir: string, manualExternals?: string[]): string[] {
156
+ logger.debug("[externals] Scanning optional peer deps...");
157
+ let stepStart = performance.now();
154
158
  const optionalPeerDeps = collectUninstalledOptionalPeerDeps(pkgDir);
159
+ logger.debug(
160
+ `[externals] Optional peer deps done: ${String(optionalPeerDeps.length)} found (${Math.round(performance.now() - stepStart)}ms)`,
161
+ );
162
+
163
+ logger.debug("[externals] Scanning native modules...");
164
+ stepStart = performance.now();
155
165
  const nativeModules = collectNativeModuleExternals(pkgDir);
166
+ logger.debug(
167
+ `[externals] Native modules done: ${String(nativeModules.length)} found (${Math.round(performance.now() - stepStart)}ms)`,
168
+ );
169
+
156
170
  const manual = manualExternals ?? [];
157
171
 
158
172
  const merged = [...new Set([...optionalPeerDeps, ...nativeModules, ...manual])];
@@ -307,20 +321,39 @@ async function build(info: ServerBuildInfo): Promise<ServerBuildResult> {
307
321
 
308
322
  try {
309
323
  // Parse tsconfig
324
+ logger.debug("[build] Parsing tsconfig...");
325
+ let stepStart = performance.now();
310
326
  const parsedConfig = parseRootTsconfig(info.cwd);
327
+ logger.debug(`[build] tsconfig parsed (${Math.round(performance.now() - stepStart)}ms)`);
328
+
329
+ stepStart = performance.now();
311
330
  const entryPoints = getPackageSourceFiles(info.pkgDir, parsedConfig);
331
+ logger.debug(
332
+ `[build] Found ${String(entryPoints.length)} source files (${Math.round(performance.now() - stepStart)}ms)`,
333
+ );
312
334
 
313
335
  // Server target is node environment
336
+ logger.debug("[build] Getting compiler options...");
337
+ stepStart = performance.now();
314
338
  const compilerOptions = await getCompilerOptionsForPackage(
315
339
  parsedConfig.options,
316
340
  "node",
317
341
  info.pkgDir,
318
342
  );
343
+ logger.debug(
344
+ `[build] Compiler options ready (${Math.round(performance.now() - stepStart)}ms)`,
345
+ );
319
346
 
320
347
  // Collect all externals (optional peer deps + native modules + manual)
348
+ logger.debug("[build] Collecting externals...");
349
+ stepStart = performance.now();
321
350
  const external = collectAllExternals(info.pkgDir, info.externals);
351
+ logger.debug(
352
+ `[build] Collected ${String(external.length)} externals (${Math.round(performance.now() - stepStart)}ms)`,
353
+ );
322
354
 
323
355
  // One-time esbuild
356
+ logger.debug("[build] Creating esbuild options...");
324
357
  const esbuildOptions = createServerEsbuildOptions({
325
358
  pkgDir: info.pkgDir,
326
359
  entryPoints,
@@ -329,17 +362,30 @@ async function build(info: ServerBuildInfo): Promise<ServerBuildResult> {
329
362
  external,
330
363
  });
331
364
 
365
+ logger.debug("[build] Running esbuild...");
366
+ stepStart = performance.now();
332
367
  const result = await esbuild.build(esbuildOptions);
368
+ logger.debug(`[build] esbuild done (${Math.round(performance.now() - stepStart)}ms)`);
333
369
 
334
370
  // Generate .config.json
335
371
  const confDistPath = path.join(info.pkgDir, "dist", ".config.json");
336
372
  fs.writeFileSync(confDistPath, JSON.stringify(info.configs ?? {}, undefined, 2));
337
373
 
338
374
  // Copy public/ to dist/ (production build: no public-dev)
375
+ logger.debug("[build] Copying public files...");
376
+ stepStart = performance.now();
339
377
  await copyPublicFiles(info.pkgDir, false);
378
+ logger.debug(
379
+ `[build] Public files copied (${Math.round(performance.now() - stepStart)}ms)`,
380
+ );
340
381
 
341
382
  // Generate production files (package.json, mise.toml, openssl.cnf, pm2.config.cjs)
383
+ logger.debug("[build] Generating production files...");
384
+ stepStart = performance.now();
342
385
  generateProductionFiles(info, external);
386
+ logger.debug(
387
+ `[build] Production files generated (${Math.round(performance.now() - stepStart)}ms)`,
388
+ );
343
389
 
344
390
  const errors = result.errors.map((e) => e.text);
345
391
  const warnings = result.warnings.map((w) => w.text);
@@ -368,26 +414,53 @@ async function createAndBuildContext(
368
414
  isFirstBuild: boolean,
369
415
  resolveFirstBuild?: () => void,
370
416
  ): Promise<esbuild.BuildContext> {
417
+ const contextStart = performance.now();
418
+
419
+ logger.debug("[context] Parsing tsconfig...");
420
+ let stepStart = performance.now();
371
421
  const parsedConfig = parseRootTsconfig(info.cwd);
422
+ logger.debug(`[context] tsconfig parsed (${Math.round(performance.now() - stepStart)}ms)`);
423
+
424
+ stepStart = performance.now();
372
425
  const entryPoints = getPackageSourceFiles(info.pkgDir, parsedConfig);
426
+ logger.debug(
427
+ `[context] Found ${String(entryPoints.length)} source files (${Math.round(performance.now() - stepStart)}ms)`,
428
+ );
429
+
430
+ logger.debug("[context] Getting compiler options...");
431
+ stepStart = performance.now();
373
432
  const compilerOptions = await getCompilerOptionsForPackage(
374
433
  parsedConfig.options,
375
434
  "node",
376
435
  info.pkgDir,
377
436
  );
437
+ logger.debug(
438
+ `[context] Compiler options ready (${Math.round(performance.now() - stepStart)}ms)`,
439
+ );
378
440
 
379
441
  const mainJsPath = path.join(info.pkgDir, "dist", "main.js");
442
+
443
+ logger.debug("[context] Collecting externals...");
444
+ stepStart = performance.now();
380
445
  const external = collectAllExternals(info.pkgDir, info.externals);
446
+ logger.debug(
447
+ `[context] Collected ${String(external.length)} externals (${Math.round(performance.now() - stepStart)}ms)`,
448
+ );
449
+
450
+ logger.debug("[context] Creating esbuild options (dev mode, minify disabled)...");
381
451
  const baseOptions = createServerEsbuildOptions({
382
452
  pkgDir: info.pkgDir,
383
453
  entryPoints,
384
454
  compilerOptions,
385
455
  env: info.env,
386
456
  external,
457
+ dev: true,
387
458
  });
388
459
 
389
460
  let isBuildFirstTime = isFirstBuild;
390
461
 
462
+ logger.debug("[context] Creating esbuild context...");
463
+ stepStart = performance.now();
391
464
  const context = await esbuild.context({
392
465
  ...baseOptions,
393
466
  metafile: true,
@@ -396,14 +469,43 @@ async function createAndBuildContext(
396
469
  {
397
470
  name: "watch-notify",
398
471
  setup(pluginBuild) {
472
+ let consecutiveStarts = 0;
473
+
399
474
  pluginBuild.onStart(() => {
400
- sender.send("buildStart", {});
475
+ consecutiveStarts++;
476
+ logger.debug(`[esbuild] onStart (#${String(consecutiveStarts)})`);
477
+
478
+ if (consecutiveStarts > 3) {
479
+ // esbuild context.rebuild() silently retries on build errors (esbuild bug).
480
+ // Stop the retry loop and diagnose via esbuild.build().
481
+ void context.dispose().catch(() => {});
482
+
483
+ void esbuild
484
+ .build(baseOptions)
485
+ .catch((err: unknown) => {
486
+ sender.send("build", {
487
+ success: false,
488
+ mainJsPath,
489
+ errors: [errNs.message(err)],
490
+ });
491
+ })
492
+ .finally(() => {
493
+ resolveFirstBuild?.();
494
+ });
495
+ } else {
496
+ sender.send("buildStart", {});
497
+ }
401
498
  });
402
499
 
403
500
  pluginBuild.onEnd(async (result) => {
501
+ consecutiveStarts = 0;
502
+
404
503
  // Save metafile
405
504
  if (result.metafile != null) {
406
505
  lastMetafile = result.metafile;
506
+ logger.debug(
507
+ `[esbuild] Metafile: ${String(Object.keys(result.metafile.inputs).length)} inputs, ${String(Object.keys(result.metafile.outputs).length)} outputs`,
508
+ );
407
509
  }
408
510
 
409
511
  const errors = result.errors.map((e) => e.text);
@@ -413,7 +515,11 @@ async function createAndBuildContext(
413
515
  // Write output files and check for changes
414
516
  let hasOutputChange = false;
415
517
  if (success && result.outputFiles != null) {
518
+ const writeStart = performance.now();
416
519
  hasOutputChange = await writeChangedOutputFiles(result.outputFiles);
520
+ logger.debug(
521
+ `[esbuild] Output files written: changed=${String(hasOutputChange)}, count=${String(result.outputFiles.length)} (${Math.round(performance.now() - writeStart)}ms)`,
522
+ );
417
523
  }
418
524
 
419
525
  if (isBuildFirstTime && success) {
@@ -442,8 +548,33 @@ async function createAndBuildContext(
442
548
  },
443
549
  ],
444
550
  });
551
+ logger.debug(
552
+ `[context] esbuild context created (${Math.round(performance.now() - stepStart)}ms)`,
553
+ );
554
+
555
+ logger.debug("[context] Running initial rebuild...");
556
+ stepStart = performance.now();
557
+ const progressTimer = setInterval(() => {
558
+ logger.debug(
559
+ `[esbuild] Still building... (${Math.round((performance.now() - stepStart) / 1000)}s elapsed)`,
560
+ );
561
+ }, 5000);
562
+ try {
563
+ await context.rebuild();
564
+ } catch {
565
+ // context.rebuild() may reject with "Cannot rebuild" when disposed
566
+ // from onStart guard. The real error is reported via esbuild.build()
567
+ // fallback in onStart, so we suppress this rejection.
568
+ } finally {
569
+ clearInterval(progressTimer);
570
+ }
571
+ logger.debug(
572
+ `[context] Initial rebuild done (${Math.round(performance.now() - stepStart)}ms)`,
573
+ );
445
574
 
446
- await context.rebuild();
575
+ logger.debug(
576
+ `[context] Total context setup: ${Math.round(performance.now() - contextStart)}ms`,
577
+ );
447
578
 
448
579
  return context;
449
580
  }
@@ -457,6 +588,9 @@ async function startWatch(info: ServerWatchInfo): Promise<void> {
457
588
  guardStartWatch();
458
589
 
459
590
  try {
591
+ const watchStart = performance.now();
592
+ logger.debug("[startWatch] Starting watch setup...");
593
+
460
594
  // Promise to wait for first build completion
461
595
  let resolveFirstBuild!: () => void;
462
596
  const firstBuildPromise = new Promise<void>((resolve) => {
@@ -464,16 +598,36 @@ async function startWatch(info: ServerWatchInfo): Promise<void> {
464
598
  });
465
599
 
466
600
  // Create initial esbuild context and build
601
+ logger.debug("[startWatch] Creating initial esbuild context...");
602
+ let stepStart = performance.now();
467
603
  esbuildContext = await createAndBuildContext(info, true, resolveFirstBuild);
604
+ logger.debug(
605
+ `[startWatch] Initial context created (${Math.round(performance.now() - stepStart)}ms)`,
606
+ );
468
607
 
469
608
  // Wait for first build completion
609
+ logger.debug("[startWatch] Waiting for first build completion...");
610
+ stepStart = performance.now();
470
611
  await firstBuildPromise;
612
+ logger.debug(
613
+ `[startWatch] First build completed (${Math.round(performance.now() - stepStart)}ms)`,
614
+ );
471
615
 
472
616
  // Watch public/ and public-dev/ (dev mode includes public-dev)
617
+ logger.debug("[startWatch] Setting up public file watcher...");
618
+ stepStart = performance.now();
473
619
  publicWatcher = await watchPublicFiles(info.pkgDir, true);
620
+ logger.debug(
621
+ `[startWatch] Public watcher ready (${Math.round(performance.now() - stepStart)}ms)`,
622
+ );
474
623
 
475
624
  // Collect watch paths based on dependencies
625
+ logger.debug("[startWatch] Collecting dependencies for watch paths...");
626
+ stepStart = performance.now();
476
627
  const { workspaceDeps, replaceDeps } = collectDeps(info.pkgDir, info.cwd, info.replaceDeps);
628
+ logger.debug(
629
+ `[startWatch] Deps collected: workspace=${String(workspaceDeps.length)}, replace=${String(replaceDeps.length)} (${Math.round(performance.now() - stepStart)}ms)`,
630
+ );
477
631
 
478
632
  const watchPaths: string[] = [];
479
633
 
@@ -496,7 +650,15 @@ async function startWatch(info: ServerWatchInfo): Promise<void> {
496
650
  }
497
651
 
498
652
  // Start FsWatcher
653
+ logger.debug(`[startWatch] Starting FsWatcher with ${String(watchPaths.length)} paths...`);
654
+ stepStart = performance.now();
499
655
  srcWatcher = await FsWatcher.watch(watchPaths);
656
+ logger.debug(
657
+ `[startWatch] FsWatcher ready (${Math.round(performance.now() - stepStart)}ms)`,
658
+ );
659
+ logger.debug(
660
+ `[startWatch] Total watch setup: ${Math.round(performance.now() - watchStart)}ms`,
661
+ );
500
662
 
501
663
  // Handle file changes
502
664
  srcWatcher.onChange({ delay: 300 }, async (changes) => {