c-next 0.2.0 → 0.2.2

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 (37) hide show
  1. package/bin/cnext.js +17 -7
  2. package/dist/index.js +139747 -0
  3. package/dist/index.js.map +7 -0
  4. package/package.json +8 -4
  5. package/src/cli/Cli.ts +5 -2
  6. package/src/cli/PathNormalizer.ts +170 -0
  7. package/src/cli/PlatformIOCommand.ts +51 -3
  8. package/src/cli/__tests__/Cli.integration.test.ts +100 -0
  9. package/src/cli/__tests__/Cli.test.ts +17 -12
  10. package/src/cli/__tests__/PathNormalizer.test.ts +411 -0
  11. package/src/cli/__tests__/PlatformIOCommand.test.ts +156 -0
  12. package/src/cli/serve/__tests__/ServeCommand.test.ts +1 -1
  13. package/src/lib/__tests__/parseWithSymbols.test.ts +228 -0
  14. package/src/lib/parseCHeader.ts +5 -1
  15. package/src/lib/parseWithSymbols.ts +62 -5
  16. package/src/lib/types/ISymbolInfo.ts +4 -0
  17. package/src/lib/utils/SymbolPathUtils.ts +87 -0
  18. package/src/lib/utils/__tests__/SymbolPathUtils.test.ts +123 -0
  19. package/src/transpiler/NodeFileSystem.ts +5 -0
  20. package/src/transpiler/logic/symbols/SymbolTable.ts +17 -0
  21. package/src/transpiler/output/codegen/CodeGenerator.ts +27 -32
  22. package/src/transpiler/output/codegen/TypeResolver.ts +12 -24
  23. package/src/transpiler/output/codegen/__tests__/CodeGenerator.coverage.test.ts +130 -5
  24. package/src/transpiler/output/codegen/__tests__/CodeGenerator.test.ts +67 -57
  25. package/src/transpiler/output/codegen/analysis/MemberChainAnalyzer.ts +9 -13
  26. package/src/transpiler/output/codegen/analysis/__tests__/MemberChainAnalyzer.test.ts +20 -10
  27. package/src/transpiler/output/codegen/assignment/AssignmentClassifier.ts +5 -2
  28. package/src/transpiler/output/codegen/assignment/__tests__/AssignmentClassifier.test.ts +18 -0
  29. package/src/transpiler/output/codegen/assignment/handlers/StringHandlers.ts +25 -4
  30. package/src/transpiler/output/codegen/assignment/handlers/__tests__/handlerTestUtils.ts +18 -0
  31. package/src/transpiler/output/codegen/generators/expressions/CallExprGenerator.ts +51 -2
  32. package/src/transpiler/output/codegen/generators/expressions/LiteralGenerator.ts +76 -8
  33. package/src/transpiler/output/codegen/generators/expressions/__tests__/CallExprGenerator.test.ts +147 -0
  34. package/src/transpiler/output/codegen/generators/expressions/__tests__/LiteralGenerator.test.ts +116 -0
  35. package/src/transpiler/output/codegen/helpers/AssignmentExpectedTypeResolver.ts +6 -5
  36. package/src/transpiler/output/codegen/helpers/__tests__/AssignmentExpectedTypeResolver.test.ts +14 -5
  37. package/src/transpiler/types/IFileSystem.ts +8 -0
@@ -0,0 +1,411 @@
1
+ /**
2
+ * Unit tests for PathNormalizer
3
+ */
4
+
5
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
6
+ import { join } from "node:path";
7
+ import { tmpdir } from "node:os";
8
+ import { describe, it, expect, beforeEach, afterEach } from "vitest";
9
+ import PathNormalizer from "../PathNormalizer";
10
+ import NodeFileSystem from "../../transpiler/NodeFileSystem";
11
+ import IFileSystem from "../../transpiler/types/IFileSystem";
12
+ import ICliConfig from "../types/ICliConfig";
13
+
14
+ // Store original environment variables at module level for all tests
15
+ const originalHome = process.env.HOME;
16
+ const originalUserProfile = process.env.USERPROFILE;
17
+
18
+ describe("PathNormalizer", () => {
19
+ describe("expandTilde", () => {
20
+ beforeEach(() => {
21
+ process.env.HOME = "/home/testuser";
22
+ process.env.USERPROFILE = "C:\\Users\\testuser";
23
+ });
24
+
25
+ afterEach(() => {
26
+ process.env.HOME = originalHome;
27
+ process.env.USERPROFILE = originalUserProfile;
28
+ });
29
+
30
+ it("expands ~/path to home directory path", () => {
31
+ const result = PathNormalizer.expandTilde("~/foo/bar");
32
+ expect(result).toBe("/home/testuser/foo/bar");
33
+ });
34
+
35
+ it("expands bare ~ to home directory", () => {
36
+ const result = PathNormalizer.expandTilde("~");
37
+ expect(result).toBe("/home/testuser");
38
+ });
39
+
40
+ it("leaves absolute paths unchanged", () => {
41
+ const result = PathNormalizer.expandTilde("/abs/path");
42
+ expect(result).toBe("/abs/path");
43
+ });
44
+
45
+ it("leaves relative paths unchanged", () => {
46
+ const result = PathNormalizer.expandTilde("relative/path");
47
+ expect(result).toBe("relative/path");
48
+ });
49
+
50
+ it("leaves mid-path tilde unchanged", () => {
51
+ const result = PathNormalizer.expandTilde("/foo/~/bar");
52
+ expect(result).toBe("/foo/~/bar");
53
+ });
54
+
55
+ it("uses USERPROFILE when HOME is not set", () => {
56
+ delete process.env.HOME;
57
+ const result = PathNormalizer.expandTilde("~/docs");
58
+ expect(result).toBe("C:\\Users\\testuser/docs");
59
+ });
60
+
61
+ it("returns path unchanged when no home env is set", () => {
62
+ delete process.env.HOME;
63
+ delete process.env.USERPROFILE;
64
+ const result = PathNormalizer.expandTilde("~/docs");
65
+ expect(result).toBe("~/docs");
66
+ });
67
+ });
68
+
69
+ describe("expandRecursive", () => {
70
+ let tempDir: string;
71
+ const fs = NodeFileSystem.instance;
72
+
73
+ beforeEach(() => {
74
+ tempDir = mkdtempSync(join(tmpdir(), "pathnorm-test-"));
75
+ // Create nested directory structure:
76
+ // tempDir/
77
+ // a/
78
+ // a1/
79
+ // a2/
80
+ // b/
81
+ mkdirSync(join(tempDir, "a", "a1"), { recursive: true });
82
+ mkdirSync(join(tempDir, "a", "a2"), { recursive: true });
83
+ mkdirSync(join(tempDir, "b"), { recursive: true });
84
+ });
85
+
86
+ afterEach(() => {
87
+ rmSync(tempDir, { recursive: true, force: true });
88
+ });
89
+
90
+ it("returns single-element array for path without ** suffix", () => {
91
+ const result = PathNormalizer.expandRecursive(tempDir, fs);
92
+ expect(result).toEqual([tempDir]);
93
+ });
94
+
95
+ it("expands path/** to all subdirectories", () => {
96
+ const result = PathNormalizer.expandRecursive(`${tempDir}/**`, fs);
97
+ expect(result).toContain(tempDir);
98
+ expect(result).toContain(join(tempDir, "a"));
99
+ expect(result).toContain(join(tempDir, "a", "a1"));
100
+ expect(result).toContain(join(tempDir, "a", "a2"));
101
+ expect(result).toContain(join(tempDir, "b"));
102
+ expect(result).toHaveLength(5);
103
+ });
104
+
105
+ it("returns empty array for nonexistent path with **", () => {
106
+ const result = PathNormalizer.expandRecursive("/nonexistent/path/**", fs);
107
+ expect(result).toEqual([]);
108
+ });
109
+
110
+ it("returns empty array for nonexistent path without **", () => {
111
+ const result = PathNormalizer.expandRecursive("/nonexistent/path", fs);
112
+ expect(result).toEqual([]);
113
+ });
114
+
115
+ it("returns single-element array if path is a file", () => {
116
+ const filePath = join(tempDir, "file.txt");
117
+ writeFileSync(filePath, "content");
118
+ const result = PathNormalizer.expandRecursive(filePath, fs);
119
+ expect(result).toEqual([filePath]);
120
+ });
121
+ });
122
+
123
+ describe("normalizePath", () => {
124
+ beforeEach(() => {
125
+ process.env.HOME = "/home/testuser";
126
+ });
127
+
128
+ afterEach(() => {
129
+ process.env.HOME = originalHome;
130
+ });
131
+
132
+ it("expands tilde in path", () => {
133
+ const result = PathNormalizer.normalizePath("~/output");
134
+ expect(result).toBe("/home/testuser/output");
135
+ });
136
+
137
+ it("leaves absolute path unchanged", () => {
138
+ const result = PathNormalizer.normalizePath("/abs/path");
139
+ expect(result).toBe("/abs/path");
140
+ });
141
+
142
+ it("handles empty string", () => {
143
+ const result = PathNormalizer.normalizePath("");
144
+ expect(result).toBe("");
145
+ });
146
+ });
147
+
148
+ describe("normalizeIncludePaths", () => {
149
+ let tempDir: string;
150
+ const fs = NodeFileSystem.instance;
151
+
152
+ beforeEach(() => {
153
+ process.env.HOME = "/home/testuser";
154
+ tempDir = mkdtempSync(join(tmpdir(), "pathnorm-include-"));
155
+ mkdirSync(join(tempDir, "sub1"), { recursive: true });
156
+ mkdirSync(join(tempDir, "sub2"), { recursive: true });
157
+ });
158
+
159
+ afterEach(() => {
160
+ process.env.HOME = originalHome;
161
+ rmSync(tempDir, { recursive: true, force: true });
162
+ });
163
+
164
+ it("expands tilde in all paths", () => {
165
+ // Use mock fs for tilde paths since they won't exist
166
+ const mockFs: IFileSystem = {
167
+ exists: (p) => p === "/home/testuser/a" || p === "/home/testuser/b",
168
+ isDirectory: (p) =>
169
+ p === "/home/testuser/a" || p === "/home/testuser/b",
170
+ readdir: () => [],
171
+ readFile: () => "",
172
+ writeFile: () => {},
173
+ mkdir: () => {},
174
+ isFile: () => false,
175
+ stat: () => ({ mtimeMs: 0 }),
176
+ };
177
+ const result = PathNormalizer.normalizeIncludePaths(
178
+ ["~/a", "~/b"],
179
+ mockFs,
180
+ );
181
+ expect(result).toEqual(["/home/testuser/a", "/home/testuser/b"]);
182
+ });
183
+
184
+ it("expands ** in paths", () => {
185
+ const result = PathNormalizer.normalizeIncludePaths(
186
+ [`${tempDir}/**`],
187
+ fs,
188
+ );
189
+ expect(result).toContain(tempDir);
190
+ expect(result).toContain(join(tempDir, "sub1"));
191
+ expect(result).toContain(join(tempDir, "sub2"));
192
+ });
193
+
194
+ it("handles mixed paths with tilde and **", () => {
195
+ const result = PathNormalizer.normalizeIncludePaths(
196
+ [tempDir, `${tempDir}/**`],
197
+ fs,
198
+ );
199
+ expect(result).toContain(tempDir);
200
+ expect(result).toContain(join(tempDir, "sub1"));
201
+ });
202
+
203
+ it("filters out nonexistent paths", () => {
204
+ const result = PathNormalizer.normalizeIncludePaths(
205
+ ["/nonexistent", tempDir],
206
+ fs,
207
+ );
208
+ expect(result).toEqual([tempDir]);
209
+ });
210
+
211
+ it("returns empty array for empty input", () => {
212
+ const result = PathNormalizer.normalizeIncludePaths([], fs);
213
+ expect(result).toEqual([]);
214
+ });
215
+
216
+ it("deduplicates paths", () => {
217
+ const result = PathNormalizer.normalizeIncludePaths(
218
+ [tempDir, tempDir, join(tempDir, "sub1"), tempDir],
219
+ fs,
220
+ );
221
+ // Each path appears only once, order preserved
222
+ expect(result).toEqual([tempDir, join(tempDir, "sub1")]);
223
+ });
224
+
225
+ it("deduplicates paths from overlapping recursive expansions", () => {
226
+ // tempDir/** expands to [tempDir, sub1, sub2]
227
+ // Adding tempDir explicitly shouldn't duplicate
228
+ const result = PathNormalizer.normalizeIncludePaths(
229
+ [tempDir, `${tempDir}/**`],
230
+ fs,
231
+ );
232
+ // tempDir should appear only once
233
+ const tempDirCount = result.filter((p) => p === tempDir).length;
234
+ expect(tempDirCount).toBe(1);
235
+ });
236
+ });
237
+
238
+ describe("expandRecursive symlink handling", () => {
239
+ it("handles symlink loops via realpath", () => {
240
+ const mockFs: IFileSystem = {
241
+ exists: () => true,
242
+ isDirectory: () => true,
243
+ // Simulate symlink loop: dir -> subdir -> (symlink to dir)
244
+ readdir: (dir) => {
245
+ if (dir === "/root") return ["subdir"];
246
+ if (dir === "/root/subdir") return ["link-to-root"];
247
+ return [];
248
+ },
249
+ // realpath resolves the symlink to its target
250
+ realpath: (path) => {
251
+ if (path === "/root/subdir/link-to-root") return "/root";
252
+ return path;
253
+ },
254
+ readFile: () => "",
255
+ writeFile: () => {},
256
+ mkdir: () => {},
257
+ isFile: () => false,
258
+ stat: () => ({ mtimeMs: 0 }),
259
+ };
260
+
261
+ const result = PathNormalizer.expandRecursive("/root/**", mockFs);
262
+
263
+ // Should include /root and /root/subdir but NOT loop infinitely
264
+ expect(result).toContain("/root");
265
+ expect(result).toContain("/root/subdir");
266
+ // The symlink directory is detected but already visited
267
+ expect(result).toHaveLength(2);
268
+ });
269
+
270
+ it("works without realpath (graceful degradation)", () => {
271
+ const mockFs: IFileSystem = {
272
+ exists: () => true,
273
+ isDirectory: (path) =>
274
+ path === "/root" || path === "/root/sub1" || path === "/root/sub2",
275
+ readdir: (dir) => {
276
+ if (dir === "/root") return ["sub1", "sub2"];
277
+ return [];
278
+ },
279
+ // No realpath method
280
+ readFile: () => "",
281
+ writeFile: () => {},
282
+ mkdir: () => {},
283
+ isFile: () => false,
284
+ stat: () => ({ mtimeMs: 0 }),
285
+ };
286
+
287
+ const result = PathNormalizer.expandRecursive("/root/**", mockFs);
288
+
289
+ expect(result).toContain("/root");
290
+ expect(result).toContain("/root/sub1");
291
+ expect(result).toContain("/root/sub2");
292
+ expect(result).toHaveLength(3);
293
+ });
294
+ });
295
+
296
+ describe("normalizeConfig", () => {
297
+ let tempDir: string;
298
+ const fs = NodeFileSystem.instance;
299
+
300
+ beforeEach(() => {
301
+ process.env.HOME = "/home/testuser";
302
+ tempDir = mkdtempSync(join(tmpdir(), "pathnorm-config-"));
303
+ mkdirSync(join(tempDir, "include", "sub"), { recursive: true });
304
+ });
305
+
306
+ afterEach(() => {
307
+ process.env.HOME = originalHome;
308
+ rmSync(tempDir, { recursive: true, force: true });
309
+ });
310
+
311
+ it("normalizes all path fields in config", () => {
312
+ const mockFs: IFileSystem = {
313
+ exists: () => true,
314
+ isDirectory: () => true,
315
+ readdir: () => [],
316
+ readFile: () => "",
317
+ writeFile: () => {},
318
+ mkdir: () => {},
319
+ isFile: () => false,
320
+ stat: () => ({ mtimeMs: 0 }),
321
+ };
322
+
323
+ const config: ICliConfig = {
324
+ inputs: ["file.cnx"],
325
+ outputPath: "~/build",
326
+ includeDirs: ["~/sdk/include"],
327
+ defines: {},
328
+ preprocess: false,
329
+ verbose: false,
330
+ cppRequired: false,
331
+ noCache: false,
332
+ parseOnly: false,
333
+ headerOutDir: "~/include",
334
+ basePath: "~/src",
335
+ };
336
+
337
+ const result = PathNormalizer.normalizeConfig(config, mockFs);
338
+
339
+ expect(result.outputPath).toBe("/home/testuser/build");
340
+ expect(result.headerOutDir).toBe("/home/testuser/include");
341
+ expect(result.basePath).toBe("/home/testuser/src");
342
+ expect(result.includeDirs).toEqual(["/home/testuser/sdk/include"]);
343
+ });
344
+
345
+ it("handles undefined optional fields", () => {
346
+ const config: ICliConfig = {
347
+ inputs: ["file.cnx"],
348
+ outputPath: "",
349
+ includeDirs: [],
350
+ defines: {},
351
+ preprocess: false,
352
+ verbose: false,
353
+ cppRequired: false,
354
+ noCache: false,
355
+ parseOnly: false,
356
+ };
357
+
358
+ const result = PathNormalizer.normalizeConfig(config);
359
+
360
+ expect(result.headerOutDir).toBeUndefined();
361
+ expect(result.basePath).toBeUndefined();
362
+ });
363
+
364
+ it("expands ** in include paths", () => {
365
+ const config: ICliConfig = {
366
+ inputs: [],
367
+ outputPath: "",
368
+ includeDirs: [`${tempDir}/include/**`],
369
+ defines: {},
370
+ preprocess: false,
371
+ verbose: false,
372
+ cppRequired: false,
373
+ noCache: false,
374
+ parseOnly: false,
375
+ };
376
+
377
+ const result = PathNormalizer.normalizeConfig(config, fs);
378
+
379
+ expect(result.includeDirs).toContain(join(tempDir, "include"));
380
+ expect(result.includeDirs).toContain(join(tempDir, "include", "sub"));
381
+ });
382
+
383
+ it("preserves non-path fields unchanged", () => {
384
+ const config: ICliConfig = {
385
+ inputs: ["a.cnx", "b.cnx"],
386
+ outputPath: "",
387
+ includeDirs: [],
388
+ defines: { DEBUG: true },
389
+ preprocess: true,
390
+ verbose: true,
391
+ cppRequired: true,
392
+ noCache: true,
393
+ parseOnly: true,
394
+ target: "teensy41",
395
+ debugMode: true,
396
+ };
397
+
398
+ const result = PathNormalizer.normalizeConfig(config);
399
+
400
+ expect(result.inputs).toEqual(["a.cnx", "b.cnx"]);
401
+ expect(result.defines).toEqual({ DEBUG: true });
402
+ expect(result.preprocess).toBe(true);
403
+ expect(result.verbose).toBe(true);
404
+ expect(result.cppRequired).toBe(true);
405
+ expect(result.noCache).toBe(true);
406
+ expect(result.parseOnly).toBe(true);
407
+ expect(result.target).toBe("teensy41");
408
+ expect(result.debugMode).toBe(true);
409
+ });
410
+ });
411
+ });
@@ -68,6 +68,30 @@ describe("PlatformIOCommand", () => {
68
68
  expect(scriptCall?.[1]).toContain("transpile_cnext");
69
69
  });
70
70
 
71
+ it("generates script that runs at import time, not as buildprog pre-action (issue #833)", () => {
72
+ // Issue #833: buildprog fires AFTER compilation, so transpile runs too late
73
+ // The script should run transpilation at import time (before compilation)
74
+ vi.mocked(fs.existsSync).mockReturnValue(true);
75
+ vi.mocked(fs.readFileSync).mockReturnValue("[env:esp32]\n");
76
+
77
+ PlatformIOCommand.install();
78
+
79
+ const writeCalls = vi.mocked(fs.writeFileSync).mock.calls;
80
+ const scriptCall = writeCalls.find((call) =>
81
+ (call[0] as string).includes("cnext_build.py"),
82
+ );
83
+
84
+ expect(scriptCall).toBeDefined();
85
+ const scriptContent = scriptCall?.[1] as string;
86
+
87
+ // Should NOT use AddPreAction (runs too late)
88
+ expect(scriptContent).not.toContain("AddPreAction");
89
+ expect(scriptContent).not.toContain("buildprog");
90
+
91
+ // Should call transpile_cnext() at top level (import time)
92
+ expect(scriptContent).toContain("transpile_cnext()");
93
+ });
94
+
71
95
  it("adds extra_scripts to platformio.ini when not present", () => {
72
96
  vi.mocked(fs.existsSync).mockReturnValue(true);
73
97
  vi.mocked(fs.readFileSync).mockReturnValue(
@@ -129,6 +153,138 @@ describe("PlatformIOCommand", () => {
129
153
  expect.stringContaining("Next steps"),
130
154
  );
131
155
  });
156
+
157
+ describe("cnext.config.json setup", () => {
158
+ it("creates cnext.config.json when it doesn't exist", () => {
159
+ vi.mocked(fs.existsSync).mockImplementation((path) => {
160
+ if ((path as string).includes("platformio.ini")) return true;
161
+ if ((path as string).includes("cnext.config.json")) return false;
162
+ return false;
163
+ });
164
+ vi.mocked(fs.readFileSync).mockReturnValue("[env:esp32]\n");
165
+
166
+ PlatformIOCommand.install();
167
+
168
+ const writeCalls = vi.mocked(fs.writeFileSync).mock.calls;
169
+ const configCall = writeCalls.find((call) =>
170
+ (call[0] as string).includes("cnext.config.json"),
171
+ );
172
+
173
+ expect(configCall).toBeDefined();
174
+ const config = JSON.parse(configCall?.[1] as string);
175
+ expect(config.include).toContain(".pio/libdeps");
176
+ expect(config.headerOut).toBe("include");
177
+ });
178
+
179
+ it("appends .pio/libdeps to existing include array", () => {
180
+ vi.mocked(fs.existsSync).mockImplementation((path) => {
181
+ if ((path as string).includes("platformio.ini")) return true;
182
+ if ((path as string).includes("cnext.config.json")) return true;
183
+ return false;
184
+ });
185
+ vi.mocked(fs.readFileSync).mockImplementation((path) => {
186
+ if ((path as string).includes("cnext.config.json")) {
187
+ return JSON.stringify({ include: ["lib/"] });
188
+ }
189
+ return "[env:esp32]\n";
190
+ });
191
+
192
+ PlatformIOCommand.install();
193
+
194
+ const writeCalls = vi.mocked(fs.writeFileSync).mock.calls;
195
+ const configCall = writeCalls.find((call) =>
196
+ (call[0] as string).includes("cnext.config.json"),
197
+ );
198
+
199
+ expect(configCall).toBeDefined();
200
+ const config = JSON.parse(configCall?.[1] as string);
201
+ expect(config.include).toContain("lib/");
202
+ expect(config.include).toContain(".pio/libdeps");
203
+ });
204
+
205
+ it("sets headerOut only if not already set", () => {
206
+ vi.mocked(fs.existsSync).mockImplementation((path) => {
207
+ if ((path as string).includes("platformio.ini")) return true;
208
+ if ((path as string).includes("cnext.config.json")) return true;
209
+ return false;
210
+ });
211
+ vi.mocked(fs.readFileSync).mockImplementation((path) => {
212
+ if ((path as string).includes("cnext.config.json")) {
213
+ return JSON.stringify({ headerOut: "custom/" });
214
+ }
215
+ return "[env:esp32]\n";
216
+ });
217
+
218
+ PlatformIOCommand.install();
219
+
220
+ const writeCalls = vi.mocked(fs.writeFileSync).mock.calls;
221
+ const configCall = writeCalls.find((call) =>
222
+ (call[0] as string).includes("cnext.config.json"),
223
+ );
224
+
225
+ expect(configCall).toBeDefined();
226
+ const config = JSON.parse(configCall?.[1] as string);
227
+ expect(config.headerOut).toBe("custom/");
228
+ });
229
+
230
+ it("does not duplicate .pio/libdeps if already present", () => {
231
+ vi.mocked(fs.existsSync).mockImplementation((path) => {
232
+ if ((path as string).includes("platformio.ini")) return true;
233
+ if ((path as string).includes("cnext.config.json")) return true;
234
+ return false;
235
+ });
236
+ vi.mocked(fs.readFileSync).mockImplementation((path) => {
237
+ if ((path as string).includes("cnext.config.json")) {
238
+ return JSON.stringify({ include: [".pio/libdeps", "lib/"] });
239
+ }
240
+ return "[env:esp32]\n";
241
+ });
242
+
243
+ PlatformIOCommand.install();
244
+
245
+ const writeCalls = vi.mocked(fs.writeFileSync).mock.calls;
246
+ const configCall = writeCalls.find((call) =>
247
+ (call[0] as string).includes("cnext.config.json"),
248
+ );
249
+
250
+ expect(configCall).toBeDefined();
251
+ const config = JSON.parse(configCall?.[1] as string);
252
+ const pioCount = config.include.filter(
253
+ (i: string) => i === ".pio/libdeps",
254
+ ).length;
255
+ expect(pioCount).toBe(1);
256
+ });
257
+
258
+ it("preserves other existing config values", () => {
259
+ vi.mocked(fs.existsSync).mockImplementation((path) => {
260
+ if ((path as string).includes("platformio.ini")) return true;
261
+ if ((path as string).includes("cnext.config.json")) return true;
262
+ return false;
263
+ });
264
+ vi.mocked(fs.readFileSync).mockImplementation((path) => {
265
+ if ((path as string).includes("cnext.config.json")) {
266
+ return JSON.stringify({
267
+ cppRequired: true,
268
+ target: "teensy41",
269
+ include: ["lib/"],
270
+ });
271
+ }
272
+ return "[env:esp32]\n";
273
+ });
274
+
275
+ PlatformIOCommand.install();
276
+
277
+ const writeCalls = vi.mocked(fs.writeFileSync).mock.calls;
278
+ const configCall = writeCalls.find((call) =>
279
+ (call[0] as string).includes("cnext.config.json"),
280
+ );
281
+
282
+ expect(configCall).toBeDefined();
283
+ const config = JSON.parse(configCall?.[1] as string);
284
+ expect(config.cppRequired).toBe(true);
285
+ expect(config.target).toBe("teensy41");
286
+ });
287
+ });
132
288
  });
133
289
 
134
290
  describe("uninstall", () => {
@@ -131,7 +131,7 @@ describe("ServeCommand", () => {
131
131
  id: 2,
132
132
  result: {
133
133
  success: true,
134
- code: expect.stringContaining("uint8_t x = 5;"),
134
+ code: expect.stringContaining("uint8_t x = 5U;"),
135
135
  errors: [],
136
136
  },
137
137
  });