@simplysm/sd-cli 14.0.64 → 14.0.66

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 (79) hide show
  1. package/dist/capacitor/capacitor-android.d.ts +2 -0
  2. package/dist/capacitor/capacitor-android.d.ts.map +1 -1
  3. package/dist/capacitor/capacitor-android.js +13 -0
  4. package/dist/capacitor/capacitor-android.js.map +1 -1
  5. package/dist/capacitor/capacitor-npm-config.d.ts.map +1 -1
  6. package/dist/capacitor/capacitor-npm-config.js +2 -6
  7. package/dist/capacitor/capacitor-npm-config.js.map +1 -1
  8. package/dist/electron/electron.d.ts.map +1 -1
  9. package/dist/electron/electron.js +1 -2
  10. package/dist/electron/electron.js.map +1 -1
  11. package/package.json +13 -13
  12. package/src/capacitor/capacitor-android.ts +14 -0
  13. package/src/capacitor/capacitor-npm-config.ts +2 -6
  14. package/src/electron/electron.ts +1 -2
  15. package/tests/angular/ngtsc-build-core.acc.spec.ts +36 -94
  16. package/tests/capacitor/capacitor-android.spec.ts +65 -28
  17. package/tests/capacitor/capacitor-build.spec.ts +40 -385
  18. package/tests/capacitor/capacitor-config-writer.acc.spec.ts +3 -17
  19. package/tests/capacitor/capacitor-config-writer.spec.ts +3 -17
  20. package/tests/capacitor/capacitor-init.spec.ts +40 -636
  21. package/tests/capacitor/capacitor-npm-config.acc.spec.ts +38 -168
  22. package/tests/capacitor/capacitor-npm-config.spec.ts +33 -71
  23. package/tests/commands/check.spec.ts +25 -36
  24. package/tests/commands/deployment-phase.acc.spec.ts +17 -26
  25. package/tests/commands/git-phase.acc.spec.ts +13 -112
  26. package/tests/commands/lint.spec.ts +7 -24
  27. package/tests/commands/post-publish-phase.acc.spec.ts +5 -10
  28. package/tests/commands/typecheck.spec.ts +43 -65
  29. package/tests/electron/electron.spec.ts +22 -46
  30. package/tests/engines/base-engine.spec.ts +4 -13
  31. package/tests/engines/engine-selection.spec.ts +14 -17
  32. package/tests/engines/engine-typecheck-selection.acc.spec.ts +13 -16
  33. package/tests/engines/esbuild-client-engine.acc.spec.ts +36 -40
  34. package/tests/engines/esbuild-client-engine.spec.ts +4 -23
  35. package/tests/engines/ngtsc-engine.spec.ts +3 -10
  36. package/tests/engines/server-esbuild-engine.spec.ts +3 -10
  37. package/tests/engines/tsc-engine.spec.ts +3 -10
  38. package/tests/esbuild/esbuild-tsc-plugin.acc.spec.ts +3 -8
  39. package/tests/esbuild/esbuild-tsc-plugin.spec.ts +3 -8
  40. package/tests/orchestrators/build-orchestrator.spec.ts +57 -102
  41. package/tests/orchestrators/dev-orchestrator.spec.ts +68 -109
  42. package/tests/orchestrators/typecheck-orchestrator.spec.ts +25 -57
  43. package/tests/orchestrators/watch-orchestrator.spec.ts +73 -99
  44. package/tests/sd-cli-entry.spec.ts +17 -20
  45. package/tests/utils/angular-source-file-cache.spec.ts +4 -8
  46. package/tests/utils/copy-src.spec.ts +9 -20
  47. package/tests/utils/esbuild-client-config.acc.spec.ts +9 -15
  48. package/tests/utils/esbuild-client-config.spec.ts +12 -24
  49. package/tests/utils/esbuild-config.spec.ts +51 -42
  50. package/tests/utils/lint-core.spec.ts +13 -19
  51. package/tests/utils/lint-utils.spec.ts +8 -15
  52. package/tests/utils/lint-with-program.spec.ts +3 -7
  53. package/tests/utils/ngtsc-build-core.spec.ts +2 -99
  54. package/tests/utils/orchestrator-utils.spec.ts +7 -20
  55. package/tests/utils/output-utils.spec.ts +5 -11
  56. package/tests/utils/sd-config.spec.ts +4 -12
  57. package/tests/utils/typecheck-env.spec.ts +49 -77
  58. package/tests/utils/typecheck-non-package.spec.ts +23 -16
  59. package/tests/workers/build-watch-paths.acc.spec.ts +4 -10
  60. package/tests/workers/build-watch-paths.spec.ts +4 -9
  61. package/tests/workers/client-worker.acc.spec.ts +64 -137
  62. package/tests/workers/client-worker.spec.ts +63 -89
  63. package/tests/workers/library-build-lint.spec.ts +19 -30
  64. package/tests/workers/library-build-worker.spec.ts +28 -55
  65. package/tests/workers/server-esbuild-context.acc.spec.ts +6 -15
  66. package/tests/workers/server-esbuild-context.spec.ts +7 -16
  67. package/tests/workers/server-runtime-worker.spec.ts +8 -10
  68. package/tests/workers/shared-worker-lifecycle.acc.spec.ts +3 -5
  69. package/tests/workers/shared-worker-lifecycle.spec.ts +4 -5
  70. package/tests/capacitor/capacitor-icon.spec.ts +0 -285
  71. package/tests/capacitor/capacitor-run.spec.ts +0 -256
  72. package/tests/capacitor/capacitor-workspace.spec.ts +0 -203
  73. package/tests/commands/device.spec.ts +0 -237
  74. package/tests/commands/publish.spec.ts +0 -1183
  75. package/tests/utils/external-modules.spec.ts +0 -217
  76. package/tests/workers/server-build-lint.spec.ts +0 -201
  77. package/tests/workers/server-build-worker.spec.ts +0 -765
  78. package/tests/workers/server-watch-manager.acc.spec.ts +0 -162
  79. package/tests/workers/server-watch-manager.spec.ts +0 -199
@@ -1,765 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from "vitest";
2
- import path from "path";
3
-
4
- //#region Mocks
5
-
6
- let workerFns: Record<string, (...args: any[]) => any>;
7
- let mockSend: ReturnType<typeof vi.fn>;
8
-
9
- // Guard reset function (set by worker-utils mock factory)
10
- let resetGuard: () => void;
11
-
12
- // fs mock tracking
13
- const writtenFiles = new Map<string, string>();
14
- const mockWriteFileSync = vi.fn((filePath: string, content: string) => {
15
- writtenFiles.set(filePath, content);
16
- });
17
- const mockReadFileSync = vi.fn();
18
- const mockExistsSync = vi.fn();
19
-
20
- // FsWatcher mock
21
- const mockOnChange = vi.fn();
22
- const mockWatcherClose = vi.fn();
23
-
24
- // esbuild context mock
25
- const mockRebuild = vi.fn();
26
- const mockDispose = vi.fn();
27
- let mockMetafileInputs: Record<string, unknown> = {};
28
-
29
- // SdTsCompiler mock (js=false path)
30
- const mockCompileAsync = vi.fn(() => Promise.resolve({
31
- program: { getSourceFiles: () => [] },
32
- builderProgram: {},
33
- isForAngular: false,
34
- affectedFiles: undefined,
35
- diagnostics: [] as unknown[],
36
- errorCount: 0,
37
- warningCount: 0,
38
- errors: undefined as string[] | undefined,
39
- emitResults: undefined,
40
- lint: undefined,
41
- scssErrors: [],
42
- scssDependencies: new Map(),
43
- }));
44
- const MockSdTsCompiler = vi.fn().mockImplementation(function () {
45
- return { compileAsync: mockCompileAsync };
46
- });
47
-
48
- const mockCpxSpawnSync = vi.fn().mockReturnValue({ stdout: "v20.11.0", stderr: "", exitCode: 0 });
49
-
50
- vi.mock("@simplysm/core-node", () => ({
51
- createWorker: vi.fn((fns: Record<string, Function>) => {
52
- workerFns = fns as any;
53
- mockSend = vi.fn();
54
- return { send: mockSend };
55
- }),
56
- FsWatcher: {
57
- watch: vi.fn(() => Promise.resolve({
58
- onChange: mockOnChange,
59
- close: mockWatcherClose,
60
- })),
61
- },
62
- pathx: {
63
- posix: vi.fn((p: string) => p.replace(/\\/g, "/")),
64
- posixResolve: vi.fn((...args: string[]) => path.resolve(...args).replace(/\\/g, "/")),
65
- },
66
- cpx: {
67
- spawn: vi.fn().mockResolvedValue({ stdout: "", stderr: "", exitCode: 0 }),
68
- spawnSync: mockCpxSpawnSync,
69
- },
70
- }));
71
-
72
- vi.mock("esbuild", () => ({
73
- default: {
74
- context: vi.fn(() => {
75
- mockRebuild.mockResolvedValue({
76
- errors: [],
77
- warnings: [],
78
- outputFiles: [],
79
- metafile: { inputs: mockMetafileInputs, outputs: {} },
80
- });
81
- return Promise.resolve({ rebuild: mockRebuild, dispose: mockDispose });
82
- }),
83
- build: vi.fn(() => Promise.resolve({
84
- errors: [],
85
- warnings: [],
86
- outputFiles: [{ path: "/workspace/packages/my-server/dist/main.js", text: "export {}" }],
87
- })),
88
- },
89
- formatMessagesSync: (messages: Array<{ text: string }>, _opts: unknown) =>
90
- messages.map((m) => m.text),
91
- }));
92
-
93
- vi.mock("fs", () => ({
94
- default: {
95
- readFileSync: (...args: unknown[]) => mockReadFileSync(...(args as [string])),
96
- writeFileSync: (...args: unknown[]) => mockWriteFileSync(...(args as [string, string])),
97
- existsSync: (...args: unknown[]) => mockExistsSync(...(args as [string])),
98
- },
99
- readFileSync: (...args: unknown[]) => mockReadFileSync(...(args as [string])),
100
- writeFileSync: (...args: unknown[]) => mockWriteFileSync(...(args as [string, string])),
101
- existsSync: (...args: unknown[]) => mockExistsSync(...(args as [string])),
102
- }));
103
-
104
-
105
- // Mock lockfile content for resolveLockedVersion
106
- let mockLockfileContent = "";
107
-
108
-
109
- vi.mock("../../src/utils/tsconfig", () => ({
110
- parseTsconfig: vi.fn(() => ({
111
- options: { target: 1, module: 99 },
112
- fileNames: ["/workspace/packages/my-server/src/main.ts"],
113
- errors: [],
114
- })),
115
- getPackageSourceFiles: vi.fn(() => ["/workspace/packages/my-server/src/main.ts"]),
116
- }));
117
-
118
- vi.mock("../../src/esbuild/esbuild-config", async (importOriginal) => {
119
- const actual = await importOriginal<typeof import("../../src/esbuild/esbuild-config")>();
120
- return {
121
- ...actual,
122
- collectAllDependencyExternals: vi.fn(() => ({ optionalPeerDeps: [], nativeModules: [] })),
123
- writeChangedOutputFiles: vi.fn(() => Promise.resolve(true)),
124
- };
125
- });
126
-
127
- vi.mock("../../src/ts-compiler/SdTsCompiler", () => ({
128
- SdTsCompiler: MockSdTsCompiler,
129
- }));
130
-
131
- // tsc plugin mock (build() js=true path uses createTscPlugin)
132
- const mockTscPlugin = {
133
- plugin: { name: "sd-tsc", setup: vi.fn() },
134
- getProgram: vi.fn(),
135
- getAffectedFiles: vi.fn(),
136
- getDiagnostics: vi.fn((): unknown[] => []),
137
- getErrors: vi.fn((): string[] | undefined => undefined),
138
- getLintResult: vi.fn((): unknown => undefined),
139
- resetBuilderProgram: vi.fn(),
140
- };
141
-
142
- vi.mock("../../src/esbuild/esbuild-tsc-plugin", () => ({
143
- createTscPlugin: vi.fn(() => mockTscPlugin),
144
- }));
145
-
146
- vi.mock("../../src/workers/shared-worker-lifecycle", () => {
147
- let guardCalled = false;
148
- resetGuard = () => { guardCalled = false; };
149
- return {
150
- setupWorkerLifecycle: vi.fn(() => ({
151
- logger: { debug: vi.fn(), warn: vi.fn() },
152
- guardStartWatch: () => {
153
- if (guardCalled) throw new Error("startWatch can only be called once per Worker");
154
- guardCalled = true;
155
- },
156
- })),
157
- };
158
- });
159
-
160
- vi.mock("../../src/deps/replace-deps/collect-deps", () => ({
161
- collectDeps: vi.fn(() => ({ workspaceDeps: [], replaceDeps: [] })),
162
- }));
163
-
164
- vi.mock("../../src/utils/copy-public", () => ({
165
- copyPublicFiles: vi.fn(() => Promise.resolve()),
166
- watchPublicFiles: vi.fn(() => Promise.resolve(undefined)),
167
- }));
168
-
169
- //#endregion
170
-
171
- // Import triggers createWorker, capturing the functions
172
- await import("../../src/workers/server-build.worker");
173
-
174
- const esbuild = (await import("esbuild")).default;
175
- const { FsWatcher } = await import("@simplysm/core-node");
176
- const { copyPublicFiles, watchPublicFiles } = await import("../../src/utils/copy-public");
177
- const { collectAllDependencyExternals } =
178
- await import("../../src/esbuild/esbuild-config");
179
-
180
- describe("server-build.worker build()", () => {
181
- const baseBuildInfo = {
182
- name: "my-server",
183
- cwd: "/workspace",
184
- pkgDir: "/workspace/packages/my-server",
185
- output: { js: true, dts: false },
186
- };
187
-
188
- beforeEach(() => {
189
- writtenFiles.clear();
190
- mockWriteFileSync.mockClear();
191
- mockReadFileSync.mockReset();
192
- mockExistsSync.mockReset();
193
- vi.mocked(esbuild.build).mockClear();
194
- vi.mocked(copyPublicFiles).mockClear();
195
- vi.mocked(collectAllDependencyExternals).mockReturnValue({
196
- optionalPeerDeps: [],
197
- nativeModules: [],
198
- });
199
- mockCompileAsync.mockResolvedValue({
200
- program: { getSourceFiles: () => [] },
201
- builderProgram: {},
202
- isForAngular: false,
203
- affectedFiles: undefined,
204
- diagnostics: [],
205
- errorCount: 0,
206
- warningCount: 0,
207
- errors: undefined,
208
- emitResults: undefined,
209
- lint: undefined,
210
- scssErrors: [],
211
- scssDependencies: new Map(),
212
- });
213
-
214
- // Reset tsc plugin mock (used for js=true path)
215
- mockTscPlugin.getProgram.mockReset();
216
- mockTscPlugin.getAffectedFiles.mockReset();
217
- mockTscPlugin.getDiagnostics.mockReset().mockReturnValue([]);
218
- mockTscPlugin.getErrors.mockReset().mockReturnValue(undefined);
219
- mockTscPlugin.getLintResult.mockReset().mockReturnValue(undefined);
220
- mockTscPlugin.resetBuilderProgram.mockReset();
221
-
222
- // Reset lockfile content and cache
223
- mockLockfileContent = "";
224
- // Clear lockfile cache (module-level variable in worker)
225
- // The cache is per-import, so re-importing clears it
226
-
227
- mockExistsSync.mockImplementation((fp: string) => {
228
- if (String(fp).endsWith("pnpm-lock.yaml")) return mockLockfileContent !== "";
229
- return false;
230
- });
231
-
232
- mockReadFileSync.mockImplementation((filePath: string) => {
233
- const fp = String(filePath);
234
- if (fp.endsWith("pnpm-lock.yaml")) {
235
- return mockLockfileContent;
236
- }
237
- if (fp.endsWith("package.json")) {
238
- return JSON.stringify({
239
- name: "@simplysm/my-server",
240
- version: "1.0.0",
241
- type: "module",
242
- dependencies: {},
243
- });
244
- }
245
- return "";
246
- });
247
- });
248
-
249
- // Acceptance: production build includes worker bundle plugin
250
- it("includes worker bundle plugin in esbuild.build() plugins", async () => {
251
- await workerFns["build"](baseBuildInfo);
252
-
253
- expect(esbuild.build).toHaveBeenCalledWith(
254
- expect.objectContaining({
255
- plugins: [
256
- expect.objectContaining({ name: "sd-worker-bundle" }),
257
- mockTscPlugin.plugin,
258
- ],
259
- }),
260
- );
261
- });
262
-
263
- // Acceptance: esbuild + typecheck parallel execution
264
- it("runs esbuild and tsc in parallel for server build", async () => {
265
- const result = await workerFns["build"](baseBuildInfo);
266
-
267
- expect(result.build.success).toBe(true);
268
- expect(result.mainJsPath).toBe(path.resolve(baseBuildInfo.pkgDir, "dist", "main.js").replace(/\\/g, "/"));
269
- });
270
-
271
- // Acceptance: type error detected via tsc plugin (js=true)
272
- it("reports typecheck error in build field", async () => {
273
- mockTscPlugin.getErrors.mockReturnValue(["TS2345: type error"]);
274
- mockTscPlugin.getDiagnostics.mockReturnValue([{ code: 2345, category: 1 }]);
275
-
276
- const result = await workerFns["build"](baseBuildInfo);
277
-
278
- expect(result.build.success).toBe(false);
279
- expect(result.build.errors).toContain("TS2345: type error");
280
- expect(result.build.diagnostics).toHaveLength(1);
281
- });
282
-
283
- // Acceptance: esbuild + tsc both error — merged
284
- it("merges esbuild and tsc errors when both fail", async () => {
285
- vi.mocked(esbuild.build).mockResolvedValueOnce({
286
- errors: [{ text: "esbuild syntax error" }],
287
- warnings: [],
288
- outputFiles: [],
289
- } as any);
290
- mockTscPlugin.getErrors.mockReturnValue(["TS2322: type mismatch"]);
291
-
292
- const result = await workerFns["build"](baseBuildInfo);
293
-
294
- expect(result.build.success).toBe(false);
295
- expect(result.build.errors).toContain("esbuild syntax error");
296
- expect(result.build.errors).toContain("TS2322: type mismatch");
297
- });
298
-
299
- // Acceptance: diagnostics from tsc plugin (js=true)
300
- it("includes diagnostics from tsc plugin in build result", async () => {
301
- mockTscPlugin.getDiagnostics.mockReturnValue([
302
- { code: 2322, category: 1, messageText: "Type mismatch" },
303
- ]);
304
-
305
- const result = await workerFns["build"](baseBuildInfo);
306
-
307
- expect(result.build.diagnostics).toHaveLength(1);
308
- expect(result.build.diagnostics[0]).toEqual(
309
- expect.objectContaining({ code: 2322 }),
310
- );
311
- });
312
-
313
- // Acceptance: js=false uses SdTsCompiler directly
314
- it("uses SdTsCompiler directly when output.js=false", async () => {
315
- mockCompileAsync.mockResolvedValueOnce({
316
- program: { getSourceFiles: () => [] },
317
- builderProgram: {},
318
- isForAngular: false,
319
- affectedFiles: undefined,
320
- diagnostics: [{ code: 2345, category: 1 }],
321
- errorCount: 1,
322
- warningCount: 0,
323
- errors: ["TS2345: type error"],
324
- emitResults: undefined,
325
- lint: undefined,
326
- scssErrors: [],
327
- scssDependencies: new Map(),
328
- });
329
-
330
- const result = await workerFns["build"]({
331
- ...baseBuildInfo,
332
- output: { js: false, dts: true },
333
- });
334
-
335
- expect(result.build.success).toBe(false);
336
- expect(result.build.errors).toContain("TS2345: type error");
337
- });
338
-
339
- // Acceptance: esbuild error detected
340
- it("reports esbuild error in build field", async () => {
341
- vi.mocked(esbuild.build).mockResolvedValueOnce({
342
- errors: [{ text: "syntax error" }],
343
- warnings: [],
344
- outputFiles: [],
345
- } as any);
346
-
347
- const result = await workerFns["build"](baseBuildInfo);
348
-
349
- expect(result.build.success).toBe(false);
350
- expect(result.build.errors).toContain("syntax error");
351
- });
352
-
353
- // Unit: esbuild exception handling
354
- it("handles esbuild exception gracefully", async () => {
355
- vi.mocked(esbuild.build).mockRejectedValueOnce(new Error("esbuild crash"));
356
-
357
- const result = await workerFns["build"](baseBuildInfo);
358
-
359
- expect(result.build.success).toBe(false);
360
- expect(result.build.errors).toContain("esbuild crash");
361
- });
362
-
363
- // --- Production artifacts ---
364
-
365
- describe("production artifacts", () => {
366
- it("writes .config.json with configs data", async () => {
367
- await workerFns["build"]({
368
- ...baseBuildInfo,
369
- configs: { db: { host: "localhost", port: 5432 } },
370
- });
371
-
372
- const configPath = path.join(baseBuildInfo.pkgDir, "dist", ".config.json");
373
- expect(writtenFiles.has(configPath)).toBe(true);
374
- expect(JSON.parse(writtenFiles.get(configPath)!)).toEqual({ db: { host: "localhost", port: 5432 } });
375
- });
376
-
377
- it("generates dist/package.json with externals using versions from pnpm-lock.yaml", async () => {
378
- vi.mocked(collectAllDependencyExternals).mockReturnValue({
379
- optionalPeerDeps: [],
380
- nativeModules: ["bcrypt"],
381
- });
382
-
383
- mockLockfileContent = [
384
- "packages:",
385
- "",
386
- " 'bcrypt@5.1.1':",
387
- " resolution: {integrity: sha512-abc}",
388
- "",
389
- " 'some-pkg@2.0.3':",
390
- " resolution: {integrity: sha512-def}",
391
- ].join("\n");
392
-
393
- mockExistsSync.mockImplementation((fp: string) => {
394
- if (String(fp).endsWith("pnpm-lock.yaml")) return true;
395
- return false;
396
- });
397
-
398
- await workerFns["build"]({
399
- ...baseBuildInfo,
400
- externals: ["some-pkg"],
401
- });
402
-
403
- const pkgJsonPath = path.join(baseBuildInfo.pkgDir, "dist", "package.json");
404
- const pkg = JSON.parse(writtenFiles.get(pkgJsonPath)!);
405
- expect(pkg.name).toBe("@simplysm/my-server");
406
- expect(pkg.dependencies["bcrypt"]).toBe("5.1.1");
407
- expect(pkg.dependencies["some-pkg"]).toBe("2.0.3");
408
- });
409
-
410
- it("generates dist/openssl.cnf with legacy provider config", async () => {
411
- await workerFns["build"](baseBuildInfo);
412
-
413
- const opensslPath = path.join(baseBuildInfo.pkgDir, "dist", "openssl.cnf");
414
- expect(writtenFiles.has(opensslPath)).toBe(true);
415
- expect(writtenFiles.get(opensslPath)!).toContain("[legacy_sect]");
416
- });
417
-
418
- it("generates dist/pm2.config.cjs when pm2 option is provided", async () => {
419
- await workerFns["build"]({
420
- ...baseBuildInfo,
421
- pm2: { name: "my-app", ignoreWatchPaths: ["logs"] },
422
- });
423
-
424
- const pm2Path = path.join(baseBuildInfo.pkgDir, "dist", "pm2.config.cjs");
425
- expect(writtenFiles.has(pm2Path)).toBe(true);
426
- expect(writtenFiles.get(pm2Path)!).toContain('"my-app"');
427
- });
428
-
429
- it("generates dist/mise.toml when packageManager=mise", async () => {
430
- mockExistsSync.mockImplementation((filePath: string) =>
431
- String(filePath).endsWith("mise.toml"),
432
- );
433
- mockReadFileSync.mockImplementation((filePath: string) => {
434
- if (String(filePath).endsWith("mise.toml")) return '[tools]\nnode = "22.5.0"';
435
- return JSON.stringify({ name: "@simplysm/my-server", version: "1.0.0", type: "module" });
436
- });
437
-
438
- await workerFns["build"]({
439
- ...baseBuildInfo,
440
- packageManager: "mise",
441
- });
442
-
443
- const misePath = path.join(baseBuildInfo.pkgDir, "dist", "mise.toml");
444
- expect(writtenFiles.get(misePath)).toContain('node = "22.5.0"');
445
- });
446
-
447
- it("adds volta field to dist/package.json when packageManager=volta", async () => {
448
- await workerFns["build"]({
449
- ...baseBuildInfo,
450
- packageManager: "volta",
451
- });
452
-
453
- const pkgJsonPath = path.join(baseBuildInfo.pkgDir, "dist", "package.json");
454
- const pkg = JSON.parse(writtenFiles.get(pkgJsonPath)!);
455
- expect(pkg.volta).toBeDefined();
456
- expect(pkg.volta.node).toBe("v20.11.0");
457
- });
458
-
459
- it("includes nativeModules and manual externals in dist/package.json but excludes missing optional peer deps", async () => {
460
- vi.mocked(collectAllDependencyExternals).mockReturnValue({
461
- optionalPeerDeps: ["opt-dep"],
462
- nativeModules: ["native-mod"],
463
- });
464
-
465
- mockLockfileContent = [
466
- "packages:",
467
- "",
468
- " 'native-mod@2.4.0':",
469
- " resolution: {integrity: sha512-b}",
470
- "",
471
- " 'manual-ext@3.1.0':",
472
- " resolution: {integrity: sha512-c}",
473
- ].join("\n");
474
-
475
- mockExistsSync.mockImplementation((fp: string) => {
476
- if (String(fp).endsWith("pnpm-lock.yaml")) return true;
477
- return false;
478
- });
479
-
480
- await workerFns["build"]({
481
- ...baseBuildInfo,
482
- externals: ["manual-ext"],
483
- });
484
-
485
- const pkgJsonPath = path.join(baseBuildInfo.pkgDir, "dist", "package.json");
486
- const pkg = JSON.parse(writtenFiles.get(pkgJsonPath)!);
487
- expect(pkg.dependencies["opt-dep"]).toBeUndefined();
488
- expect(pkg.dependencies["native-mod"]).toBe("2.4.0");
489
- expect(pkg.dependencies["manual-ext"]).toBe("3.1.0");
490
- });
491
-
492
- // Unit: reports error when a nativeModule/manual external is missing from lockfile
493
- it("reports error for external dependency not in lockfile", async () => {
494
- vi.mocked(collectAllDependencyExternals).mockReturnValue({
495
- optionalPeerDeps: [],
496
- nativeModules: ["unknown-native"],
497
- });
498
-
499
- mockLockfileContent = [
500
- "packages:",
501
- "",
502
- " 'other-pkg@1.0.0':",
503
- " resolution: {integrity: sha512-abc}",
504
- ].join("\n");
505
-
506
- mockExistsSync.mockImplementation((fp: string) => {
507
- if (String(fp).endsWith("pnpm-lock.yaml")) return true;
508
- return false;
509
- });
510
-
511
- const result = await workerFns["build"](baseBuildInfo);
512
- expect(result.build.success).toBe(false);
513
- expect(result.build.errors[0]).toContain("unknown-native");
514
- expect(result.build.errors[0]).toContain("not found in pnpm-lock.yaml");
515
- });
516
-
517
- // Unit: uses locked version for native module externals
518
- it("uses locked version for native module externals", async () => {
519
- vi.mocked(collectAllDependencyExternals).mockReturnValue({
520
- optionalPeerDeps: [],
521
- nativeModules: ["native-opt"],
522
- });
523
-
524
- mockLockfileContent = [
525
- "packages:",
526
- "",
527
- " 'native-opt@4.2.1':",
528
- " resolution: {integrity: sha512-xyz}",
529
- ].join("\n");
530
-
531
- mockExistsSync.mockImplementation((fp: string) => {
532
- if (String(fp).endsWith("pnpm-lock.yaml")) return true;
533
- return false;
534
- });
535
-
536
- await workerFns["build"](baseBuildInfo);
537
-
538
- const pkgJsonPath = path.join(baseBuildInfo.pkgDir, "dist", "package.json");
539
- const pkg = JSON.parse(writtenFiles.get(pkgJsonPath)!);
540
- expect(pkg.dependencies["native-opt"]).toBe("4.2.1");
541
- });
542
- });
543
-
544
- });
545
-
546
- describe("server-build.worker startWatch()", () => {
547
- const watchInfo = {
548
- name: "my-server",
549
- cwd: "/workspace",
550
- pkgDir: "/workspace/packages/my-server",
551
- output: { js: true, dts: false },
552
- };
553
-
554
- beforeEach(() => {
555
- resetGuard();
556
- mockMetafileInputs = {};
557
- writtenFiles.clear();
558
- mockWriteFileSync.mockClear();
559
- mockRebuild.mockClear();
560
- mockDispose.mockClear();
561
- mockOnChange.mockClear();
562
- mockWatcherClose.mockClear();
563
- mockSend.mockClear();
564
- vi.mocked(esbuild.context).mockClear();
565
- vi.mocked(FsWatcher.watch).mockClear();
566
- vi.mocked(watchPublicFiles).mockClear();
567
- mockCompileAsync.mockResolvedValue({
568
- program: { getSourceFiles: () => [] },
569
- builderProgram: {},
570
- isForAngular: false,
571
- affectedFiles: undefined,
572
- diagnostics: [],
573
- errorCount: 0,
574
- warningCount: 0,
575
- errors: undefined,
576
- emitResults: undefined,
577
- lint: undefined,
578
- scssErrors: [],
579
- scssDependencies: new Map(),
580
- });
581
-
582
- // Reset tsc plugin mock (used for watch mode rebuild)
583
- mockTscPlugin.getProgram.mockReset();
584
- mockTscPlugin.getAffectedFiles.mockReset();
585
- mockTscPlugin.getDiagnostics.mockReset().mockReturnValue([]);
586
- mockTscPlugin.getErrors.mockReset().mockReturnValue(undefined);
587
- mockTscPlugin.resetBuilderProgram.mockReset();
588
-
589
- mockReadFileSync.mockImplementation((filePath: string) => {
590
- if (String(filePath).endsWith("package.json")) {
591
- return JSON.stringify({
592
- name: "@simplysm/my-server",
593
- version: "1.0.0",
594
- type: "module",
595
- });
596
- }
597
- return "";
598
- });
599
- });
600
-
601
- // Acceptance: initial build with typecheck
602
- it("sends build event with build results after initial build", async () => {
603
- await workerFns["startWatch"](watchInfo);
604
-
605
- expect(mockSend).toHaveBeenCalledWith("build", expect.objectContaining({
606
- build: expect.objectContaining({ success: true }),
607
- mainJsPath: path.resolve(watchInfo.pkgDir, "dist", "main.js").replace(/\\/g, "/"),
608
- }));
609
- });
610
-
611
- // Acceptance: metafile-based filtering
612
- it("skips rebuild when changed file is not in metafile.inputs", async () => {
613
- mockMetafileInputs = { "packages/my-server/src/main.ts": {} };
614
-
615
- await workerFns["startWatch"](watchInfo);
616
-
617
- const onChangeHandler = mockOnChange.mock.calls[0][1] as (
618
- changes: Array<{ event: string; path: string }>,
619
- ) => Promise<void>;
620
-
621
- mockRebuild.mockClear();
622
- mockSend.mockClear();
623
- const absPath = path.resolve("/workspace", "packages/my-server/src/unrelated.ts").replace(/\\/g, "/");
624
- await onChangeHandler([{ event: "change", path: absPath }]);
625
-
626
- // buildStart must NOT be sent when rebuild is skipped (LOGIC-001 fix)
627
- expect(mockSend).not.toHaveBeenCalledWith("buildStart", expect.anything());
628
- });
629
-
630
- // Acceptance: watch mode doesn't generate production artifacts
631
- it("writes .config.json but not production files in watch mode", async () => {
632
- await workerFns["startWatch"]({
633
- ...watchInfo,
634
- configs: { key: "value" },
635
- });
636
-
637
- const configPath = path.join(watchInfo.pkgDir, "dist", ".config.json");
638
- expect(writtenFiles.has(configPath)).toBe(true);
639
-
640
- // Production files should NOT be generated
641
- const pkgJsonPath = path.join(watchInfo.pkgDir, "dist", "package.json");
642
- expect(writtenFiles.has(pkgJsonPath)).toBe(false);
643
- });
644
-
645
- // Unit: guard prevents duplicate startWatch
646
- it("prevents duplicate startWatch calls", async () => {
647
- await workerFns["startWatch"](watchInfo);
648
- await expect(workerFns["startWatch"](watchInfo)).rejects.toThrow("can only be called once");
649
- });
650
-
651
- // Acceptance: esbuild context creation failure leaves safe state (LOGIC-001)
652
- it("allows tsc-only rebuilds after esbuild context recreation failure", async () => {
653
- // Provide metafile inputs so subsequent change passes the metafile filter
654
- mockMetafileInputs = { "packages/my-server/src/main.ts": {} };
655
-
656
- await workerFns["startWatch"](watchInfo);
657
-
658
- const onChangeHandler = mockOnChange.mock.calls[0][1] as (
659
- changes: Array<{ event: string; path: string }>,
660
- ) => Promise<void>;
661
-
662
- // After dispose, rebuild should throw (simulates real disposed context)
663
- mockDispose.mockImplementation(() => {
664
- mockRebuild.mockRejectedValue(new Error("Build context already disposed"));
665
- });
666
-
667
- // Make context() throw to simulate creation failure
668
- vi.mocked(esbuild.context).mockRejectedValueOnce(new Error("context creation failed"));
669
- mockSend.mockClear();
670
-
671
- // File add triggers context recreation → fails → sends "error"
672
- await onChangeHandler([{ event: "add", path: "/workspace/packages/my-server/src/new.ts" }]);
673
-
674
- // Subsequent file change should work without "disposed" errors
675
- mockSend.mockClear();
676
- const absPath = path.resolve("/workspace", "packages/my-server/src/main.ts").replace(/\\/g, "/");
677
- await onChangeHandler([{ event: "change", path: absPath }]);
678
-
679
- // Should NOT get "disposed" error
680
- const errorCalls = mockSend.mock.calls.filter((c) => c[0] === "error");
681
- for (const [, data] of errorCalls) {
682
- expect((data as { message: string }).message).not.toContain("disposed");
683
- }
684
- // Build event should be sent (tsc-only result)
685
- const buildCalls = mockSend.mock.calls.filter((c) => c[0] === "build");
686
- expect(buildCalls.length).toBeGreaterThanOrEqual(1);
687
- });
688
-
689
- // Acceptance: rebuildAll js=true — single esbuildCtx.rebuild() call, tsc not called directly
690
- it("uses esbuildCtx.rebuild() without direct tsc call in watch mode rebuild", async () => {
691
- mockMetafileInputs = { "packages/my-server/src/main.ts": {} };
692
-
693
- await workerFns["startWatch"](watchInfo);
694
-
695
- const onChangeHandler = mockOnChange.mock.calls[0][1] as (
696
- changes: Array<{ event: string; path: string }>,
697
- ) => Promise<void>;
698
-
699
- mockRebuild.mockClear();
700
- mockSend.mockClear();
701
-
702
- const absPath = path.resolve("/workspace", "packages/my-server/src/main.ts").replace(/\\/g, "/");
703
- await onChangeHandler([{ event: "change", path: absPath }]);
704
-
705
- // esbuild rebuild should have been called (tsc triggered by plugin inside)
706
- expect(mockRebuild).toHaveBeenCalled();
707
- // Build event should be sent
708
- expect(mockSend).toHaveBeenCalledWith("build", expect.objectContaining({
709
- build: expect.objectContaining({ success: true }),
710
- }));
711
- });
712
-
713
- // Acceptance: startWatch passes tsc options to createContext with worker plugin
714
- it("passes worker bundle plugin and tsc plugin to esbuildCtx.createContext", async () => {
715
- await workerFns["startWatch"]({
716
- ...watchInfo,
717
- output: { js: true, dts: true, env: "node" as any, includeTests: true },
718
- });
719
-
720
- expect(esbuild.context).toHaveBeenCalledWith(
721
- expect.objectContaining({
722
- plugins: [
723
- expect.objectContaining({ name: "sd-worker-bundle" }),
724
- mockTscPlugin.plugin,
725
- ],
726
- }),
727
- );
728
- });
729
- });
730
-
731
- describe("server-build.worker stopWatch()", () => {
732
- beforeEach(() => {
733
- resetGuard();
734
- mockDispose.mockClear();
735
- mockWatcherClose.mockClear();
736
- mockCompileAsync.mockResolvedValue({
737
- program: { getSourceFiles: () => [] },
738
- builderProgram: {},
739
- isForAngular: false,
740
- affectedFiles: undefined,
741
- diagnostics: [],
742
- errorCount: 0,
743
- warningCount: 0,
744
- errors: undefined,
745
- emitResults: undefined,
746
- lint: undefined,
747
- scssErrors: [],
748
- scssDependencies: new Map(),
749
- });
750
- mockReadFileSync.mockImplementation(() => JSON.stringify({ name: "x", version: "1.0.0", type: "module" }));
751
- });
752
-
753
- it("cleans up esbuild context and FsWatcher", async () => {
754
- await workerFns["startWatch"]({
755
- name: "my-server",
756
- cwd: "/workspace",
757
- pkgDir: "/workspace/packages/my-server",
758
- output: { js: true, dts: false },
759
- });
760
- await workerFns["stopWatch"]();
761
-
762
- expect(mockDispose).toHaveBeenCalled();
763
- expect(mockWatcherClose).toHaveBeenCalled();
764
- });
765
- });