c-next 0.2.11 → 0.2.13

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 (75) hide show
  1. package/README.md +80 -649
  2. package/dist/index.js +8273 -6357
  3. package/dist/index.js.map +4 -4
  4. package/grammar/C.g4 +17 -4
  5. package/package.json +3 -3
  6. package/src/__tests__/index.test.ts +1 -1
  7. package/src/cli/CleanCommand.ts +8 -12
  8. package/src/cli/Cli.ts +29 -6
  9. package/src/cli/Runner.ts +42 -62
  10. package/src/cli/__tests__/CleanCommand.test.ts +10 -10
  11. package/src/cli/__tests__/Cli.test.ts +59 -7
  12. package/src/cli/__tests__/ConfigPrinter.test.ts +12 -12
  13. package/src/cli/__tests__/PathNormalizer.test.ts +5 -5
  14. package/src/cli/__tests__/Runner.test.ts +108 -82
  15. package/src/cli/serve/ServeCommand.ts +1 -1
  16. package/src/cli/types/ICliConfig.ts +2 -2
  17. package/src/lib/parseWithSymbols.ts +21 -21
  18. package/src/transpiler/Transpiler.ts +99 -46
  19. package/src/transpiler/__tests__/DualCodePaths.test.ts +29 -29
  20. package/src/transpiler/__tests__/Transpiler.coverage.test.ts +244 -72
  21. package/src/transpiler/__tests__/Transpiler.test.ts +32 -72
  22. package/src/transpiler/__tests__/determineProjectRoot.test.ts +30 -28
  23. package/src/transpiler/__tests__/needsConditionalPreprocessing.test.ts +1 -1
  24. package/src/transpiler/data/CNextMarkerDetector.ts +34 -0
  25. package/src/transpiler/data/CppEntryPointScanner.ts +174 -0
  26. package/src/transpiler/data/FileDiscovery.ts +2 -105
  27. package/src/transpiler/data/InputExpansion.ts +37 -81
  28. package/src/transpiler/data/__tests__/CNextMarkerDetector.test.ts +62 -0
  29. package/src/transpiler/data/__tests__/CppEntryPointScanner.test.ts +239 -0
  30. package/src/transpiler/data/__tests__/FileDiscovery.test.ts +45 -191
  31. package/src/transpiler/data/__tests__/InputExpansion.test.ts +36 -204
  32. package/src/transpiler/logic/analysis/InitializationAnalyzer.ts +2 -2
  33. package/src/transpiler/logic/analysis/PassByValueAnalyzer.ts +4 -5
  34. package/src/transpiler/logic/parser/c/grammar/C.interp +33 -3
  35. package/src/transpiler/logic/parser/c/grammar/C.tokens +237 -207
  36. package/src/transpiler/logic/parser/c/grammar/CLexer.interp +48 -3
  37. package/src/transpiler/logic/parser/c/grammar/CLexer.tokens +237 -207
  38. package/src/transpiler/logic/parser/c/grammar/CLexer.ts +702 -611
  39. package/src/transpiler/logic/parser/c/grammar/CParser.ts +1221 -1107
  40. package/src/transpiler/logic/symbols/SymbolTable.ts +147 -73
  41. package/src/transpiler/logic/symbols/__tests__/SymbolTable.test.ts +157 -14
  42. package/src/transpiler/logic/symbols/c/__tests__/CResolver.integration.test.ts +3 -3
  43. package/src/transpiler/logic/symbols/c/collectors/StructCollector.ts +7 -37
  44. package/src/transpiler/logic/symbols/cnext/__tests__/TSymbolInfoAdapter.test.ts +6 -6
  45. package/src/transpiler/logic/symbols/cnext/adapters/TSymbolInfoAdapter.ts +28 -27
  46. package/src/transpiler/logic/symbols/cnext/index.ts +4 -4
  47. package/src/transpiler/logic/symbols/cnext/utils/SymbolNameUtils.ts +5 -5
  48. package/src/transpiler/output/codegen/CodeGenerator.ts +16 -1
  49. package/src/transpiler/output/codegen/__tests__/CodeGenerator.test.ts +15 -0
  50. package/src/transpiler/output/codegen/__tests__/ExpressionWalker.test.ts +3 -3
  51. package/src/transpiler/output/codegen/__tests__/RequireInclude.test.ts +14 -14
  52. package/src/transpiler/output/codegen/__tests__/TrackVariableTypeHelpers.test.ts +2 -2
  53. package/src/transpiler/output/codegen/helpers/FunctionContextManager.ts +15 -3
  54. package/src/transpiler/output/codegen/helpers/ParameterInputAdapter.ts +14 -6
  55. package/src/transpiler/output/codegen/helpers/__tests__/ParameterInputAdapter.test.ts +1 -0
  56. package/src/transpiler/output/codegen/types/IFunctionContextCallbacks.ts +2 -0
  57. package/src/transpiler/output/codegen/utils/QualifiedNameGenerator.ts +7 -7
  58. package/src/transpiler/output/codegen/utils/__tests__/QualifiedNameGenerator.test.ts +3 -3
  59. package/src/transpiler/output/headers/BaseHeaderGenerator.ts +10 -1
  60. package/src/transpiler/output/headers/HeaderGenerator.ts +3 -0
  61. package/src/transpiler/output/headers/HeaderGeneratorUtils.ts +6 -2
  62. package/src/transpiler/output/headers/__tests__/HeaderGeneratorUtils.test.ts +16 -0
  63. package/src/transpiler/output/headers/adapters/HeaderSymbolAdapter.ts +19 -19
  64. package/src/transpiler/output/headers/adapters/__tests__/HeaderSymbolAdapter.test.ts +5 -5
  65. package/src/transpiler/state/SymbolRegistry.ts +10 -12
  66. package/src/transpiler/state/__tests__/SymbolRegistry.test.ts +11 -13
  67. package/src/transpiler/types/ICachedFileEntry.ts +4 -0
  68. package/src/transpiler/types/IPipelineFile.ts +3 -0
  69. package/src/transpiler/types/ITranspilerConfig.ts +2 -2
  70. package/src/transpiler/types/symbols/IScopeSymbol.ts +1 -1
  71. package/src/utils/FunctionUtils.ts +3 -3
  72. package/src/utils/__tests__/FunctionUtils.test.ts +6 -4
  73. package/src/utils/cache/CacheManager.ts +28 -15
  74. package/src/utils/cache/__tests__/CacheManager.test.ts +6 -4
  75. package/src/transpiler/data/types/IDiscoveryOptions.ts +0 -15
package/grammar/C.g4 CHANGED
@@ -74,7 +74,7 @@ unaryExpression
74
74
  : ('++' | '--' | 'sizeof')* (
75
75
  postfixExpression
76
76
  | unaryOperator castExpression
77
- | ('sizeof' | '_Alignof') '(' typeName ')'
77
+ | ('sizeof' | '_Alignof' | '__alignof__' | '__alignof') '(' typeName ')'
78
78
  | '&&' Identifier // GCC extension address of label
79
79
  )
80
80
  ;
@@ -200,6 +200,7 @@ storageClassSpecifier
200
200
  | 'extern'
201
201
  | 'static'
202
202
  | '_Thread_local'
203
+ | '__thread' // GCC extension
203
204
  | 'auto'
204
205
  | 'register'
205
206
  ;
@@ -213,18 +214,23 @@ typeSpecifier
213
214
  | 'float'
214
215
  | 'double'
215
216
  | 'signed'
217
+ | '__signed' // GCC extension
218
+ | '__signed__' // GCC extension
216
219
  | 'unsigned'
217
220
  | '_Bool'
218
221
  | '_Complex'
219
222
  | '__m128'
220
223
  | '__m128d'
221
224
  | '__m128i'
225
+ | '__int128' // GCC 128-bit integer
226
+ | '__int128_t' // GCC 128-bit signed
227
+ | '__uint128_t' // GCC 128-bit unsigned
222
228
  | '__extension__' '(' ('__m128' | '__m128d' | '__m128i') ')'
223
229
  | atomicTypeSpecifier
224
230
  | structOrUnionSpecifier
225
231
  | enumSpecifier
226
232
  | typedefName
227
- | '__typeof__' '(' constantExpression ')' // GCC extension
233
+ | ('__typeof__' | '__typeof') '(' constantExpression ')' // GCC extension
228
234
  ;
229
235
 
230
236
  structOrUnionSpecifier
@@ -283,14 +289,21 @@ atomicTypeSpecifier
283
289
 
284
290
  typeQualifier
285
291
  : 'const'
292
+ | '__const' // GCC extension
293
+ | '__const__' // GCC extension
286
294
  | 'restrict'
295
+ | '__restrict' // GCC extension
296
+ | '__restrict__' // GCC extension
287
297
  | 'volatile'
298
+ | '__volatile' // GCC extension
299
+ | '__volatile__' // GCC extension
288
300
  | '_Atomic'
289
301
  ;
290
302
 
291
303
  functionSpecifier
292
304
  : 'inline'
293
305
  | '_Noreturn'
306
+ | '__inline' // GCC extension
294
307
  | '__inline__' // GCC extension
295
308
  | '__stdcall'
296
309
  | gccAttributeSpecifier
@@ -329,7 +342,7 @@ vcSpecificModifer
329
342
  ;
330
343
 
331
344
  gccDeclaratorExtension
332
- : '__asm' '(' StringLiteral+ ')'
345
+ : ('__asm' | '__asm__') '(' StringLiteral+ ')' // GCC asm in declarators
333
346
  | gccAttributeSpecifier
334
347
  ;
335
348
 
@@ -431,7 +444,7 @@ statement
431
444
  | selectionStatement
432
445
  | iterationStatement
433
446
  | jumpStatement
434
- | ('__asm' | '__asm__') ('volatile' | '__volatile__') '(' (
447
+ | ('__asm' | '__asm__') ('volatile' | '__volatile' | '__volatile__') '(' (
435
448
  logicalOrExpression (',' logicalOrExpression)*
436
449
  )? (':' (logicalOrExpression (',' logicalOrExpression)*)?)* ')' ';'
437
450
  ;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "c-next",
3
- "version": "0.2.11",
3
+ "version": "0.2.13",
4
4
  "description": "A safer C for embedded systems development. Transpiles to clean, readable C.",
5
5
  "packageManager": "npm@11.9.0",
6
6
  "type": "module",
@@ -92,12 +92,12 @@
92
92
  "devDependencies": {
93
93
  "@types/node": "^25.2.1",
94
94
  "@types/yargs": "^17.0.35",
95
- "esbuild": "^0.27.3",
96
95
  "@vitest/coverage-v8": "^4.0.18",
97
96
  "antlr4ng-cli": "^2.0.0",
98
97
  "chalk": "^5.6.2",
99
98
  "cspell": "^9.6.4",
100
99
  "dependency-cruiser": "^17.3.7",
100
+ "esbuild": "^0.27.3",
101
101
  "husky": "^9.1.7",
102
102
  "knip": "^5.83.1",
103
103
  "lint-staged": "^16.2.7",
@@ -111,8 +111,8 @@
111
111
  "@n1ru4l/toposort": "^0.0.1",
112
112
  "antlr4ng": "^3.0.16",
113
113
  "cosmiconfig": "^9.0.0",
114
- "fast-glob": "^3.3.3",
115
114
  "flat-cache": "^6.1.20",
115
+ "immer": "^11.1.4",
116
116
  "tsx": "^4.21.0",
117
117
  "typescript": "^5.9.3",
118
118
  "yargs": "^18.0.0"
@@ -48,7 +48,7 @@ describe("index.ts (CLI entry point)", () => {
48
48
 
49
49
  it("calls Runner.execute when shouldRun is true", async () => {
50
50
  const mockConfig = {
51
- inputs: ["test.cnx"],
51
+ input: "test.cnx",
52
52
  outputPath: "",
53
53
  includeDirs: [],
54
54
  defines: {},
@@ -3,7 +3,7 @@
3
3
  * Deletes generated files (.c, .cpp, .h, .hpp) that have matching .cnx sources
4
4
  */
5
5
 
6
- import { basename, join, resolve } from "node:path";
6
+ import { basename, dirname, join, resolve } from "node:path";
7
7
  import { unlinkSync } from "node:fs";
8
8
  import InputExpansion from "../transpiler/data/InputExpansion";
9
9
  import PathResolver from "../transpiler/data/PathResolver";
@@ -13,12 +13,12 @@ import PathResolver from "../transpiler/data/PathResolver";
13
13
  */
14
14
  class CleanCommand {
15
15
  /**
16
- * Discover CNX files from inputs.
16
+ * Discover CNX files from entry point.
17
17
  * Returns null if none found or on error.
18
18
  */
19
- private static discoverCnxFiles(inputs: string[]): string[] | null {
19
+ private static discoverCnxFiles(input: string): string[] | null {
20
20
  try {
21
- const cnxFiles = InputExpansion.expandInputs(inputs);
21
+ const cnxFiles = InputExpansion.expandInputs([input]);
22
22
  if (cnxFiles.length === 0) {
23
23
  console.log("No .cnx files found. Nothing to clean.");
24
24
  return null;
@@ -54,21 +54,17 @@ class CleanCommand {
54
54
  /**
55
55
  * Execute the clean command
56
56
  *
57
- * @param inputs - Input files or directories (source locations)
57
+ * @param input - Entry point .cnx file
58
58
  * @param outDir - Output directory for code files
59
59
  * @param headerOutDir - Optional separate output directory for headers
60
60
  */
61
- static execute(
62
- inputs: string[],
63
- outDir: string,
64
- headerOutDir?: string,
65
- ): void {
61
+ static execute(input: string, outDir: string, headerOutDir?: string): void {
66
62
  if (!outDir) {
67
63
  console.log("No output directory specified. Nothing to clean.");
68
64
  return;
69
65
  }
70
66
 
71
- const cnxFiles = this.discoverCnxFiles(inputs);
67
+ const cnxFiles = this.discoverCnxFiles(input);
72
68
  if (!cnxFiles) return;
73
69
 
74
70
  const resolvedOutDir = resolve(outDir);
@@ -77,7 +73,7 @@ class CleanCommand {
77
73
  : resolvedOutDir;
78
74
 
79
75
  const pathResolver = new PathResolver({
80
- inputs,
76
+ inputs: [dirname(resolve(input))],
81
77
  outDir,
82
78
  headerOutDir,
83
79
  });
package/src/cli/Cli.ts CHANGED
@@ -5,6 +5,7 @@
5
5
  */
6
6
 
7
7
  import { dirname, resolve } from "node:path";
8
+ import { existsSync, statSync } from "node:fs";
8
9
  import ArgParser from "./ArgParser";
9
10
  import ConfigLoader from "./ConfigLoader";
10
11
  import ConfigPrinter from "./ConfigPrinter";
@@ -65,17 +66,39 @@ class Cli {
65
66
  return { shouldRun: false, exitCode: 0 };
66
67
  }
67
68
 
68
- // Validate inputs
69
- if (config.inputs.length === 0) {
70
- console.error("Error: No input files specified");
71
- console.error("Run 'cnext --help' for usage information");
69
+ // Validate single entry point
70
+ if (args.inputFiles.length > 1) {
71
+ console.error("Error: Only one entry point file is supported");
72
+ console.error(
73
+ "Other files are discovered automatically via #include directives",
74
+ );
75
+ return { shouldRun: false, exitCode: 1 };
76
+ }
77
+
78
+ if (!config.input) {
79
+ console.error("Error: No input file specified");
80
+ console.error("Usage: cnext <file.cnx>");
81
+ return { shouldRun: false, exitCode: 1 };
82
+ }
83
+
84
+ const resolvedInput = resolve(config.input);
85
+ if (!existsSync(resolvedInput)) {
86
+ console.error(`Error: Input not found: ${config.input}`);
87
+ return { shouldRun: false, exitCode: 1 };
88
+ }
89
+
90
+ if (statSync(resolvedInput).isDirectory()) {
91
+ console.error(
92
+ "Error: Directory input not supported. Specify an entry point file.",
93
+ );
94
+ console.error(`Example: cnext ${config.input}/main.cnx`);
72
95
  return { shouldRun: false, exitCode: 1 };
73
96
  }
74
97
 
75
98
  // Handle --clean: delete generated files
76
99
  if (args.cleanMode) {
77
100
  CleanCommand.execute(
78
- config.inputs,
101
+ config.input,
79
102
  config.outputPath,
80
103
  config.headerOutDir,
81
104
  );
@@ -94,7 +117,7 @@ class Cli {
94
117
  fileConfig: IFileConfig,
95
118
  ): ICliConfig {
96
119
  const rawConfig: ICliConfig = {
97
- inputs: args.inputFiles,
120
+ input: args.inputFiles[0] ?? "",
98
121
  outputPath: args.outputPath || fileConfig.output || "",
99
122
  // Merge include dirs: config includes come first, CLI includes override/append
100
123
  includeDirs: [...(fileConfig.include ?? []), ...args.includeDirs],
package/src/cli/Runner.ts CHANGED
@@ -3,19 +3,13 @@
3
3
  * Executes the transpiler with the given configuration
4
4
  */
5
5
 
6
- import { dirname, resolve } from "node:path";
6
+ import { basename, dirname, resolve } from "node:path";
7
7
  import { existsSync, statSync, renameSync } from "node:fs";
8
- import InputExpansion from "../transpiler/data/InputExpansion";
9
8
  import Transpiler from "../transpiler/Transpiler";
10
9
  import ICliConfig from "./types/ICliConfig";
11
10
  import ResultPrinter from "./ResultPrinter";
12
11
  import ITranspilerResult from "../transpiler/types/ITranspilerResult";
13
-
14
- /** Result of categorizing inputs into directories and files */
15
- interface ICategorizedInputs {
16
- srcDirs: string[];
17
- explicitFiles: string[];
18
- }
12
+ import InputExpansion from "../transpiler/data/InputExpansion";
19
13
 
20
14
  /** Result of determining output path */
21
15
  interface IOutputPathResult {
@@ -32,19 +26,21 @@ class Runner {
32
26
  * @param config - CLI configuration
33
27
  */
34
28
  static async execute(config: ICliConfig): Promise<void> {
35
- const { srcDirs, explicitFiles } = this._categorizeInputs(config.inputs);
36
- const files = this._expandInputFiles(config.inputs);
29
+ const resolvedInput = resolve(config.input);
37
30
  const { outDir, explicitOutputFile } = this._determineOutputPath(
38
31
  config,
39
- files,
32
+ resolvedInput,
40
33
  );
41
34
 
35
+ // Infer basePath from entry file's parent directory if not set
36
+ const basePath = config.basePath || dirname(resolvedInput);
37
+
42
38
  const pipeline = new Transpiler({
43
- inputs: [...srcDirs, ...explicitFiles],
39
+ input: resolvedInput,
44
40
  includeDirs: config.includeDirs,
45
41
  outDir,
46
42
  headerOutDir: config.headerOutDir,
47
- basePath: config.basePath,
43
+ basePath,
48
44
  preprocess: config.preprocess,
49
45
  defines: config.defines,
50
46
  cppRequired: config.cppRequired,
@@ -54,52 +50,20 @@ class Runner {
54
50
  debugMode: config.debugMode,
55
51
  });
56
52
 
57
- const result = await pipeline.transpile({ kind: "files" });
58
- this._renameOutputIfNeeded(result, explicitOutputFile);
59
-
60
- ResultPrinter.print(result);
61
- process.exit(result.success ? 0 : 1);
62
- }
63
-
64
- /**
65
- * Categorize inputs into directories and explicit files.
66
- */
67
- private static _categorizeInputs(inputs: string[]): ICategorizedInputs {
68
- const srcDirs: string[] = [];
69
- const explicitFiles: string[] = [];
70
-
71
- for (const input of inputs) {
72
- const resolvedPath = resolve(input);
73
- const isDir =
74
- existsSync(resolvedPath) && statSync(resolvedPath).isDirectory();
75
- if (isDir) {
76
- srcDirs.push(resolvedPath);
77
- } else {
78
- explicitFiles.push(resolvedPath);
79
- }
53
+ if (InputExpansion.isCppEntryPoint(resolvedInput)) {
54
+ console.log(`Scanning ${basename(resolvedInput)} for C-Next includes...`);
80
55
  }
81
56
 
82
- return { srcDirs, explicitFiles };
83
- }
57
+ const result = await pipeline.transpile({ kind: "files" });
84
58
 
85
- /**
86
- * Expand input paths to .cnx files.
87
- */
88
- private static _expandInputFiles(inputs: string[]): string[] {
89
- let files: string[];
90
- try {
91
- files = InputExpansion.expandInputs(inputs);
92
- } catch (error) {
93
- console.error(`Error: ${error}`);
94
- process.exit(1);
59
+ if (InputExpansion.isCppEntryPoint(resolvedInput)) {
60
+ this._printCppEntryPointResult(result, resolvedInput);
95
61
  }
96
62
 
97
- if (files.length === 0) {
98
- console.error("Error: No .cnx files found");
99
- process.exit(1);
100
- }
63
+ this._renameOutputIfNeeded(result, explicitOutputFile);
101
64
 
102
- return files;
65
+ ResultPrinter.print(result);
66
+ process.exit(result.success ? 0 : 1);
103
67
  }
104
68
 
105
69
  /**
@@ -107,10 +71,10 @@ class Runner {
107
71
  */
108
72
  private static _determineOutputPath(
109
73
  config: ICliConfig,
110
- files: string[],
74
+ resolvedInput: string,
111
75
  ): IOutputPathResult {
112
76
  if (!config.outputPath) {
113
- return { outDir: dirname(files[0]), explicitOutputFile: null };
77
+ return { outDir: dirname(resolvedInput), explicitOutputFile: null };
114
78
  }
115
79
 
116
80
  const isExplicitFile =
@@ -127,13 +91,6 @@ class Runner {
127
91
 
128
92
  // Explicit output file
129
93
  if (isExplicitFile) {
130
- if (files.length > 1) {
131
- console.error(
132
- "Error: Cannot use explicit output filename with multiple input files",
133
- );
134
- console.error("Use a directory path instead: -o <directory>/");
135
- process.exit(1);
136
- }
137
94
  return {
138
95
  outDir: dirname(config.outputPath),
139
96
  explicitOutputFile: resolve(config.outputPath),
@@ -165,6 +122,29 @@ class Runner {
165
122
  result.outputFiles[0] = explicitOutputFile;
166
123
  }
167
124
  }
125
+
126
+ /**
127
+ * Print result message for C/C++ entry point scanning.
128
+ */
129
+ private static _printCppEntryPointResult(
130
+ result: ITranspilerResult,
131
+ resolvedInput: string,
132
+ ): void {
133
+ if (result.filesProcessed === 0) {
134
+ console.log("No C-Next files found in include tree. To get started:");
135
+ console.log(" 1. Create a .cnx file (e.g., led.cnx)");
136
+ console.log(" 2. Run: cnext led.cnx");
137
+ console.log(" 3. Include the generated header in your C/C++ code");
138
+ console.log(` 4. Re-run: cnext ${basename(resolvedInput)}`);
139
+ } else {
140
+ const fileNames = result.files
141
+ .map((f) => basename(f.sourcePath))
142
+ .join(", ");
143
+ console.log(
144
+ `Found ${result.filesProcessed} C-Next source file(s): ${fileNames}`,
145
+ );
146
+ }
147
+ }
168
148
  }
169
149
 
170
150
  export default Runner;
@@ -35,7 +35,7 @@ describe("CleanCommand", () => {
35
35
 
36
36
  describe("execute", () => {
37
37
  it("reports no output directory when outDir is empty", () => {
38
- CleanCommand.execute(["src/"], "", undefined);
38
+ CleanCommand.execute("src/", "", undefined);
39
39
 
40
40
  expect(consoleLogSpy).toHaveBeenCalledWith(
41
41
  "No output directory specified. Nothing to clean.",
@@ -45,7 +45,7 @@ describe("CleanCommand", () => {
45
45
  it("reports no files found when no .cnx files exist", () => {
46
46
  vi.mocked(InputExpansion.expandInputs).mockReturnValue([]);
47
47
 
48
- CleanCommand.execute(["src/"], "build/", undefined);
48
+ CleanCommand.execute("src/", "build/", undefined);
49
49
 
50
50
  expect(consoleLogSpy).toHaveBeenCalledWith(
51
51
  "No .cnx files found. Nothing to clean.",
@@ -57,7 +57,7 @@ describe("CleanCommand", () => {
57
57
  throw new Error("Path not found");
58
58
  });
59
59
 
60
- CleanCommand.execute(["nonexistent/"], "build/", undefined);
60
+ CleanCommand.execute("nonexistent/", "build/", undefined);
61
61
 
62
62
  expect(consoleErrorSpy).toHaveBeenCalledWith(
63
63
  "Error: Error: Path not found",
@@ -72,7 +72,7 @@ describe("CleanCommand", () => {
72
72
  // Mock unlinkSync to succeed for all calls
73
73
  vi.mocked(fs.unlinkSync).mockImplementation(() => {});
74
74
 
75
- CleanCommand.execute(["/project/src/"], "/project/build/", undefined);
75
+ CleanCommand.execute("/project/src/", "/project/build/", undefined);
76
76
 
77
77
  // Should try to delete .c, .cpp, .h, .hpp files
78
78
  expect(fs.unlinkSync).toHaveBeenCalledTimes(4);
@@ -89,7 +89,7 @@ describe("CleanCommand", () => {
89
89
  vi.mocked(fs.unlinkSync).mockImplementation(() => {});
90
90
 
91
91
  CleanCommand.execute(
92
- ["/project/src/"],
92
+ "/project/src/",
93
93
  "/project/build/",
94
94
  "/project/include/",
95
95
  );
@@ -127,7 +127,7 @@ describe("CleanCommand", () => {
127
127
  throw enoentError;
128
128
  });
129
129
 
130
- CleanCommand.execute(["/project/src/"], "/project/build/", undefined);
130
+ CleanCommand.execute("/project/src/", "/project/build/", undefined);
131
131
 
132
132
  expect(consoleLogSpy).toHaveBeenCalledWith(
133
133
  "No generated files found to delete.",
@@ -146,7 +146,7 @@ describe("CleanCommand", () => {
146
146
  throw permError;
147
147
  });
148
148
 
149
- CleanCommand.execute(["/project/src/"], "/project/build/", undefined);
149
+ CleanCommand.execute("/project/src/", "/project/build/", undefined);
150
150
 
151
151
  expect(consoleErrorSpy).toHaveBeenCalled();
152
152
  expect(consoleLogSpy).toHaveBeenCalledWith(
@@ -168,7 +168,7 @@ describe("CleanCommand", () => {
168
168
  vi.mocked(fs.unlinkSync).mockImplementation(() => {});
169
169
 
170
170
  CleanCommand.execute(
171
- ["/project/src/main.cnx"],
171
+ "/project/src/main.cnx",
172
172
  "/project/build/",
173
173
  undefined,
174
174
  );
@@ -187,7 +187,7 @@ describe("CleanCommand", () => {
187
187
 
188
188
  vi.mocked(fs.unlinkSync).mockImplementation(() => {});
189
189
 
190
- CleanCommand.execute(["/project/src/"], "/project/build/", undefined);
190
+ CleanCommand.execute("/project/src/", "/project/build/", undefined);
191
191
 
192
192
  const unlinkCalls = vi.mocked(fs.unlinkSync).mock.calls;
193
193
  const paths = unlinkCalls.map((call) => call[0] as string);
@@ -204,7 +204,7 @@ describe("CleanCommand", () => {
204
204
 
205
205
  vi.mocked(fs.unlinkSync).mockImplementation(() => {});
206
206
 
207
- CleanCommand.execute(["/project/src/"], "/project/build/", undefined);
207
+ CleanCommand.execute("/project/src/", "/project/build/", undefined);
208
208
 
209
209
  const unlinkCalls = vi.mocked(fs.unlinkSync).mock.calls;
210
210
  const paths = unlinkCalls.map((call) => call[0] as string);
@@ -10,6 +10,7 @@
10
10
  */
11
11
 
12
12
  import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
13
+ import { existsSync, statSync } from "node:fs";
13
14
  import Cli from "../Cli";
14
15
  import ArgParser from "../ArgParser";
15
16
  import ConfigLoader from "../ConfigLoader";
@@ -31,6 +32,14 @@ vi.mock("../PathNormalizer", () => ({
31
32
  normalizeConfig: (config: unknown) => config,
32
33
  },
33
34
  }));
35
+ vi.mock("node:fs", async (importOriginal) => {
36
+ const actual = await importOriginal<typeof import("node:fs")>();
37
+ return {
38
+ ...actual,
39
+ existsSync: vi.fn(() => true),
40
+ statSync: vi.fn(() => ({ isDirectory: () => false })),
41
+ };
42
+ });
34
43
 
35
44
  describe("Cli", () => {
36
45
  let mockParsedArgs: IParsedArgs;
@@ -62,6 +71,10 @@ describe("Cli", () => {
62
71
  // Default mocks
63
72
  vi.mocked(ArgParser.parse).mockReturnValue(mockParsedArgs);
64
73
  vi.mocked(ConfigLoader.load).mockReturnValue({});
74
+ vi.mocked(existsSync).mockReturnValue(true);
75
+ vi.mocked(statSync).mockReturnValue({
76
+ isDirectory: () => false,
77
+ } as ReturnType<typeof statSync>);
65
78
 
66
79
  consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
67
80
  });
@@ -71,13 +84,13 @@ describe("Cli", () => {
71
84
  });
72
85
 
73
86
  describe("run", () => {
74
- it("returns shouldRun: true with config when inputs are provided", () => {
87
+ it("returns shouldRun: true with config when input is provided", () => {
75
88
  const result = Cli.run();
76
89
 
77
90
  expect(result.shouldRun).toBe(true);
78
91
  expect(result.exitCode).toBe(0);
79
92
  expect(result.config).toBeDefined();
80
- expect(result.config?.inputs).toEqual(["input.cnx"]);
93
+ expect(result.config?.input).toBe("input.cnx");
81
94
  });
82
95
 
83
96
  it("handles --pio-install flag", () => {
@@ -135,7 +148,46 @@ describe("Cli", () => {
135
148
  expect(result.exitCode).toBe(0);
136
149
  });
137
150
 
138
- it("returns error when no input files specified", () => {
151
+ it("returns error when multiple input files specified", () => {
152
+ mockParsedArgs.inputFiles = ["a.cnx", "b.cnx"];
153
+ vi.mocked(ArgParser.parse).mockReturnValue(mockParsedArgs);
154
+
155
+ const result = Cli.run();
156
+
157
+ expect(result.shouldRun).toBe(false);
158
+ expect(result.exitCode).toBe(1);
159
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
160
+ "Error: Only one entry point file is supported",
161
+ );
162
+ });
163
+
164
+ it("returns error when input file does not exist", () => {
165
+ vi.mocked(existsSync).mockReturnValue(false);
166
+
167
+ const result = Cli.run();
168
+
169
+ expect(result.shouldRun).toBe(false);
170
+ expect(result.exitCode).toBe(1);
171
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
172
+ expect.stringContaining("Input not found"),
173
+ );
174
+ });
175
+
176
+ it("returns error when input is a directory", () => {
177
+ vi.mocked(statSync).mockReturnValue({
178
+ isDirectory: () => true,
179
+ } as ReturnType<typeof statSync>);
180
+
181
+ const result = Cli.run();
182
+
183
+ expect(result.shouldRun).toBe(false);
184
+ expect(result.exitCode).toBe(1);
185
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
186
+ "Error: Directory input not supported. Specify an entry point file.",
187
+ );
188
+ });
189
+
190
+ it("returns error when no input file specified", () => {
139
191
  mockParsedArgs.inputFiles = [];
140
192
  vi.mocked(ArgParser.parse).mockReturnValue(mockParsedArgs);
141
193
 
@@ -144,7 +196,7 @@ describe("Cli", () => {
144
196
  expect(result.shouldRun).toBe(false);
145
197
  expect(result.exitCode).toBe(1);
146
198
  expect(consoleErrorSpy).toHaveBeenCalledWith(
147
- "Error: No input files specified",
199
+ "Error: No input file specified",
148
200
  );
149
201
  });
150
202
 
@@ -157,7 +209,7 @@ describe("Cli", () => {
157
209
  expect(ConfigLoader.load).toHaveBeenCalledWith("/path/to/src");
158
210
  });
159
211
 
160
- it("loads config from cwd when no input files", () => {
212
+ it("loads config from cwd when no input file", () => {
161
213
  mockParsedArgs.inputFiles = [];
162
214
  mockParsedArgs.showConfig = true; // To avoid error path
163
215
  vi.mocked(ArgParser.parse).mockReturnValue(mockParsedArgs);
@@ -294,7 +346,7 @@ describe("Cli", () => {
294
346
 
295
347
  it("passes through all config fields", () => {
296
348
  mockParsedArgs = {
297
- inputFiles: ["a.cnx", "b.cnx"],
349
+ inputFiles: ["a.cnx"],
298
350
  outputPath: "build/",
299
351
  includeDirs: ["inc/"],
300
352
  defines: { DEBUG: true },
@@ -318,7 +370,7 @@ describe("Cli", () => {
318
370
  const result = Cli.run();
319
371
 
320
372
  expect(result.config).toEqual({
321
- inputs: ["a.cnx", "b.cnx"],
373
+ input: "a.cnx",
322
374
  outputPath: "build/",
323
375
  includeDirs: ["inc/"],
324
376
  defines: { DEBUG: true },