c-next 0.1.58 → 0.1.60

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 (140) hide show
  1. package/README.md +2 -3
  2. package/package.json +13 -4
  3. package/src/cli/ArgParser.ts +12 -0
  4. package/src/cli/CleanCommand.ts +56 -44
  5. package/src/cli/Cli.ts +10 -0
  6. package/src/cli/PlatformIOCommand.ts +27 -18
  7. package/src/cli/Runner.ts +115 -69
  8. package/src/cli/__tests__/ArgParser.test.ts +16 -12
  9. package/src/cli/__tests__/Cli.test.ts +13 -0
  10. package/src/cli/serve/JsonRpcHandler.ts +155 -0
  11. package/src/cli/serve/ServeCommand.ts +236 -0
  12. package/src/cli/serve/__tests__/JsonRpcHandler.test.ts +199 -0
  13. package/src/cli/serve/__tests__/ServeCommand.test.ts +224 -0
  14. package/src/cli/serve/types/IJsonRpcRequest.ts +13 -0
  15. package/src/cli/serve/types/IJsonRpcResponse.ts +18 -0
  16. package/src/cli/types/ICliResult.ts +4 -0
  17. package/src/cli/types/IParsedArgs.ts +2 -0
  18. package/src/index.ts +7 -0
  19. package/src/lib/__tests__/transpiler.test.ts +60 -0
  20. package/src/lib/parseWithSymbols.ts +8 -1
  21. package/src/lib/transpiler.ts +19 -154
  22. package/src/lib/types/TSymbolKind.ts +4 -0
  23. package/src/transpiler/Transpiler.ts +469 -310
  24. package/src/transpiler/__tests__/DualCodePaths.test.ts +110 -0
  25. package/src/transpiler/__tests__/Transpiler.coverage.test.ts +1 -1
  26. package/src/transpiler/__tests__/Transpiler.test.ts +34 -0
  27. package/src/transpiler/data/DependencyGraph.ts +1 -1
  28. package/src/transpiler/data/IncludeDiscovery.ts +42 -35
  29. package/src/transpiler/data/IncludeResolver.ts +157 -88
  30. package/src/transpiler/logic/analysis/FloatModuloAnalyzer.ts +15 -16
  31. package/src/transpiler/logic/analysis/GrammarCoverageListener.ts +7 -34
  32. package/src/transpiler/logic/analysis/InitializationAnalyzer.ts +120 -88
  33. package/src/transpiler/logic/analysis/__tests__/CommentExtractor.test.ts +54 -0
  34. package/src/transpiler/logic/analysis/__tests__/DivisionByZeroAnalyzer.test.ts +49 -0
  35. package/src/transpiler/logic/analysis/__tests__/FloatModuloAnalyzer.test.ts +64 -0
  36. package/src/transpiler/logic/analysis/__tests__/FunctionCallAnalyzer.test.ts +49 -0
  37. package/src/transpiler/logic/analysis/__tests__/GrammarCoverageListener.test.ts +281 -0
  38. package/src/transpiler/logic/analysis/__tests__/InitializationAnalyzer.test.ts +618 -0
  39. package/src/transpiler/logic/analysis/__tests__/NullCheckAnalyzer.test.ts +131 -0
  40. package/src/transpiler/logic/analysis/__tests__/ScopeStack.test.ts +15 -0
  41. package/src/transpiler/logic/analysis/__tests__/StructFieldAnalyzer.test.ts +159 -0
  42. package/src/transpiler/logic/analysis/__tests__/runAnalyzers.test.ts +304 -0
  43. package/src/transpiler/logic/analysis/runAnalyzers.ts +66 -105
  44. package/src/transpiler/logic/analysis/types/GrammarCoverageReportBuilder.ts +61 -0
  45. package/src/transpiler/logic/symbols/CSymbolCollector.ts +104 -61
  46. package/src/transpiler/logic/symbols/CppSymbolCollector.ts +240 -136
  47. package/src/transpiler/logic/symbols/__tests__/CppSymbolCollector.test.ts +65 -0
  48. package/src/transpiler/logic/symbols/__tests__/TransitiveEnumCollector.test.ts +129 -0
  49. package/src/transpiler/logic/symbols/cnext/__tests__/StructCollector.test.ts +22 -0
  50. package/src/transpiler/logic/symbols/cnext/__tests__/TSymbolAdapter.test.ts +16 -1
  51. package/src/transpiler/logic/symbols/cnext/adapters/TSymbolAdapter.ts +32 -7
  52. package/src/transpiler/logic/symbols/cnext/adapters/TSymbolInfoAdapter.ts +54 -31
  53. package/src/transpiler/logic/symbols/cnext/collectors/BitmapCollector.ts +1 -0
  54. package/src/transpiler/logic/symbols/cnext/collectors/EnumCollector.ts +1 -0
  55. package/src/transpiler/logic/symbols/cnext/collectors/FunctionCollector.ts +1 -0
  56. package/src/transpiler/logic/symbols/cnext/collectors/RegisterCollector.ts +1 -0
  57. package/src/transpiler/logic/symbols/cnext/collectors/StructCollector.ts +33 -24
  58. package/src/transpiler/logic/symbols/cnext/collectors/VariableCollector.ts +69 -37
  59. package/src/transpiler/logic/symbols/cnext/index.ts +154 -98
  60. package/src/transpiler/logic/symbols/types/IBaseSymbol.ts +3 -0
  61. package/src/transpiler/output/codegen/CodeGenerator.ts +1790 -1877
  62. package/src/transpiler/output/codegen/TypeResolver.ts +3 -51
  63. package/src/transpiler/output/codegen/TypeValidator.ts +201 -188
  64. package/src/transpiler/output/codegen/__tests__/CodeGenerator.test.ts +6818 -0
  65. package/src/transpiler/output/codegen/analysis/MemberChainAnalyzer.ts +41 -89
  66. package/src/transpiler/output/codegen/analysis/__tests__/MemberChainAnalyzer.test.ts +164 -0
  67. package/src/transpiler/output/codegen/assignment/AssignmentClassifier.ts +429 -242
  68. package/src/transpiler/output/codegen/assignment/__tests__/AssignmentClassifier.test.ts +271 -0
  69. package/src/transpiler/output/codegen/assignment/handlers/AccessPatternHandlers.ts +12 -2
  70. package/src/transpiler/output/codegen/assignment/handlers/AssignmentHandlerUtils.ts +124 -0
  71. package/src/transpiler/output/codegen/assignment/handlers/BitmapHandlers.ts +11 -4
  72. package/src/transpiler/output/codegen/assignment/handlers/IRegisterNameResult.ts +14 -0
  73. package/src/transpiler/output/codegen/assignment/handlers/RegisterHandlers.ts +81 -90
  74. package/src/transpiler/output/codegen/assignment/handlers/RegisterUtils.ts +26 -0
  75. package/src/transpiler/output/codegen/assignment/handlers/__tests__/AssignmentHandlerRegistry.test.ts +81 -0
  76. package/src/transpiler/output/codegen/assignment/handlers/__tests__/AssignmentHandlerUtils.test.ts +164 -0
  77. package/src/transpiler/output/codegen/assignment/handlers/index.ts +12 -26
  78. package/src/transpiler/output/codegen/assignment/index.ts +10 -10
  79. package/src/transpiler/output/codegen/generators/IOrchestrator.ts +2 -4
  80. package/src/transpiler/output/codegen/generators/declarationGenerators/BitmapCommentUtils.ts +61 -0
  81. package/src/transpiler/output/codegen/generators/declarationGenerators/BitmapGenerator.ts +3 -12
  82. package/src/transpiler/output/codegen/generators/declarationGenerators/ScopeGenerator.ts +327 -242
  83. package/src/transpiler/output/codegen/generators/declarationGenerators/__tests__/BitmapCommentUtils.test.ts +52 -0
  84. package/src/transpiler/output/codegen/generators/expressions/AccessExprGenerator.ts +202 -156
  85. package/src/transpiler/output/codegen/generators/expressions/BinaryExprGenerator.ts +60 -45
  86. package/src/transpiler/output/codegen/generators/expressions/BitmapAccessHelper.ts +57 -0
  87. package/src/transpiler/output/codegen/generators/expressions/CallExprGenerator.ts +115 -105
  88. package/src/transpiler/output/codegen/generators/expressions/CallExprUtils.ts +42 -0
  89. package/src/transpiler/output/codegen/generators/expressions/PostfixExpressionGenerator.ts +1097 -591
  90. package/src/transpiler/output/codegen/generators/expressions/__tests__/BitmapAccessHelper.test.ts +115 -0
  91. package/src/transpiler/output/codegen/generators/expressions/__tests__/CallExprGenerator.test.ts +1257 -0
  92. package/src/transpiler/output/codegen/generators/expressions/__tests__/CallExprUtils.test.ts +145 -1
  93. package/src/transpiler/output/codegen/generators/expressions/__tests__/LiteralGenerator.test.ts +168 -0
  94. package/src/transpiler/output/codegen/generators/expressions/__tests__/PostfixExpressionGenerator.test.ts +146 -11
  95. package/src/transpiler/output/codegen/generators/statements/AtomicGenerator.ts +34 -35
  96. package/src/transpiler/output/codegen/generators/statements/ControlFlowGenerator.ts +47 -151
  97. package/src/transpiler/output/codegen/generators/statements/SwitchGenerator.ts +100 -72
  98. package/src/transpiler/output/codegen/generators/support/HelperGenerator.ts +5 -271
  99. package/src/transpiler/output/codegen/generators/support/OverflowHelperTemplates.ts +409 -0
  100. package/src/transpiler/output/codegen/generators/support/__tests__/OverflowHelperTemplates.test.ts +182 -0
  101. package/src/transpiler/output/codegen/helpers/ArrayDimensionParser.ts +167 -87
  102. package/src/transpiler/output/codegen/helpers/ArrayInitHelper.ts +95 -61
  103. package/src/transpiler/output/codegen/helpers/AssignmentValidator.ts +16 -6
  104. package/src/transpiler/output/codegen/helpers/BitRangeHelper.ts +89 -0
  105. package/src/transpiler/output/codegen/helpers/BooleanHelper.ts +40 -0
  106. package/src/transpiler/output/codegen/helpers/CodeGenErrors.ts +77 -0
  107. package/src/transpiler/output/codegen/helpers/CppConstructorHelper.ts +75 -0
  108. package/src/transpiler/output/codegen/helpers/CppMemberHelper.ts +185 -0
  109. package/src/transpiler/output/codegen/helpers/IntegerLiteralValidator.ts +93 -0
  110. package/src/transpiler/output/codegen/helpers/MemberAccessValidator.ts +87 -0
  111. package/src/transpiler/output/codegen/helpers/SetMapHelper.ts +63 -0
  112. package/src/transpiler/output/codegen/helpers/StringDeclHelper.ts +205 -150
  113. package/src/transpiler/output/codegen/helpers/SymbolLookupHelper.ts +131 -0
  114. package/src/transpiler/output/codegen/helpers/VariableModifierBuilder.ts +108 -0
  115. package/src/transpiler/output/codegen/helpers/__tests__/AssignmentValidator.test.ts +34 -0
  116. package/src/transpiler/output/codegen/helpers/__tests__/BitRangeHelper.test.ts +157 -0
  117. package/src/transpiler/output/codegen/helpers/__tests__/BooleanHelper.test.ts +68 -0
  118. package/src/transpiler/output/codegen/helpers/__tests__/CodeGenErrors.test.ts +132 -0
  119. package/src/transpiler/output/codegen/helpers/__tests__/CppConstructorHelper.test.ts +139 -0
  120. package/src/transpiler/output/codegen/helpers/__tests__/CppMemberHelper.test.ts +377 -0
  121. package/src/transpiler/output/codegen/helpers/__tests__/EnumAssignmentValidator.test.ts +31 -0
  122. package/src/transpiler/output/codegen/helpers/__tests__/IntegerLiteralValidator.test.ts +170 -0
  123. package/src/transpiler/output/codegen/helpers/__tests__/MemberAccessValidator.test.ts +185 -0
  124. package/src/transpiler/output/codegen/helpers/__tests__/SetMapHelper.test.ts +140 -0
  125. package/src/transpiler/output/codegen/helpers/__tests__/SymbolLookupHelper.test.ts +326 -0
  126. package/src/transpiler/output/codegen/helpers/__tests__/VariableModifierBuilder.test.ts +164 -0
  127. package/src/transpiler/output/codegen/helpers/types/IPostfixOp.ts +12 -0
  128. package/src/transpiler/output/codegen/memberAccessChain.ts +242 -115
  129. package/src/transpiler/output/codegen/utils/ExpressionUnwrapper.ts +166 -0
  130. package/src/transpiler/output/codegen/utils/__tests__/ExpressionUnwrapper.test.ts +182 -0
  131. package/src/transpiler/output/headers/BaseHeaderGenerator.ts +225 -0
  132. package/src/transpiler/output/headers/CHeaderGenerator.ts +5 -196
  133. package/src/transpiler/output/headers/CppHeaderGenerator.ts +5 -196
  134. package/src/transpiler/output/headers/HeaderGeneratorUtils.ts +21 -50
  135. package/src/transpiler/output/headers/__tests__/BaseHeaderGenerator.test.ts +245 -0
  136. package/src/transpiler/output/headers/generators/__tests__/generateStructHeader.test.ts +41 -0
  137. package/src/transpiler/output/headers/generators/generateStructHeader.ts +9 -1
  138. package/src/utils/ExpressionUtils.ts +17 -3
  139. package/src/utils/ParserUtils.ts +40 -0
  140. package/src/utils/__tests__/ParserUtils.test.ts +80 -0
package/README.md CHANGED
@@ -1,10 +1,9 @@
1
1
  # C-Next
2
2
 
3
- [![CI](https://github.com/jlaustill/c-next/actions/workflows/pr-checks.yml/badge.svg)](https://github.com/jlaustill/c-next/actions/workflows/pr-checks.yml)
4
3
  [![npm version](https://img.shields.io/npm/v/c-next)](https://www.npmjs.com/package/c-next)
5
- [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
6
5
  [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=jlaustill_c-next&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=jlaustill_c-next)
7
- [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=jlaustill_c-next&metric=coverage)](https://sonarcloud.io/summary/new_code?id=jlaustill_c-next)
6
+ [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=jlaustill_c-next&metric=coverage)](https://sonarcloud.io/summary/overall?id=jlaustill_c-next)
8
7
  [![Coverage Report](https://img.shields.io/badge/Coverage-Report-blue)](https://jlaustill.github.io/c-next/coverage/)
9
8
  [![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=jlaustill_c-next&metric=sqale_rating)](https://sonarcloud.io/summary/new_code?id=jlaustill_c-next)
10
9
 
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "c-next",
3
- "version": "0.1.58",
3
+ "version": "0.1.60",
4
4
  "description": "A safer C for embedded systems development. Transpiles to clean, readable C.",
5
+ "packageManager": "npm@11.9.0",
5
6
  "type": "module",
6
7
  "main": "src/index.ts",
7
8
  "bin": {
@@ -48,7 +49,13 @@
48
49
  "duplication": "npx jscpd src/ scripts/ --reporters console",
49
50
  "duplication:json": "npx jscpd src/ scripts/ --reporters json --output .jscpd",
50
51
  "duplication:sonar": "curl -s 'https://sonarcloud.io/api/measures/component_tree?component=jlaustill_c-next&metricKeys=duplicated_blocks,duplicated_lines&s=metric&metricSort=duplicated_blocks&metricSortFilter=withMeasuresOnly&asc=false&ps=20' | jq -r '.components[] | \"\\(.path)\\t\\(.measures[0].value) blocks\\t\\(.measures[1].value) lines\"'",
51
- "duplication:all": "echo '=== Local (jscpd) ===' && npm run duplication && echo '\n=== SonarCloud ===' && npm run duplication:sonar"
52
+ "duplication:all": "echo '=== Local (jscpd) ===' && npm run duplication && echo '\n=== SonarCloud ===' && npm run duplication:sonar",
53
+ "analyze:prune": "ts-prune",
54
+ "analyze:madge": "madge src/ --circular --extensions ts",
55
+ "analyze:madge:graph": "madge src/ --image docs/architecture/dependency-graph.svg --extensions ts --exclude '(parser/(grammar|c/grammar|cpp/grammar)|__tests__)' --layout sfdp",
56
+ "analyze:cpd": "./scripts/cpd.sh",
57
+ "analyze:duplication": "npx jscpd src/ scripts/",
58
+ "analyze:all": "npm run analyze:duplication && npm run analyze:prune && npm run analyze:madge"
52
59
  },
53
60
  "keywords": [
54
61
  "c",
@@ -78,7 +85,7 @@
78
85
  "LICENSE"
79
86
  ],
80
87
  "devDependencies": {
81
- "@types/node": "^25.2.0",
88
+ "@types/node": "^25.2.1",
82
89
  "@types/yargs": "^17.0.35",
83
90
  "@vitest/coverage-v8": "^4.0.18",
84
91
  "antlr4ng-cli": "^2.0.0",
@@ -86,10 +93,12 @@
86
93
  "cspell": "^9.6.4",
87
94
  "dependency-cruiser": "^17.3.7",
88
95
  "husky": "^9.1.7",
89
- "knip": "^5.83.0",
96
+ "knip": "^5.83.1",
90
97
  "lint-staged": "^16.2.7",
98
+ "madge": "^8.0.0",
91
99
  "oxlint": "^1.43.0",
92
100
  "prettier": "^3.8.1",
101
+ "ts-prune": "^0.10.3",
93
102
  "vitest": "^4.0.18"
94
103
  },
95
104
  "dependencies": {
@@ -30,6 +30,7 @@ interface IYargsResult {
30
30
  cache: boolean;
31
31
  "pio-install": boolean;
32
32
  "pio-uninstall": boolean;
33
+ serve: boolean;
33
34
  }
34
35
 
35
36
  /**
@@ -145,6 +146,13 @@ A safer C for embedded systems development.`,
145
146
  default: false,
146
147
  })
147
148
 
149
+ // Server mode
150
+ .option("serve", {
151
+ type: "boolean",
152
+ describe: "Start JSON-RPC server on stdin/stdout",
153
+ default: false,
154
+ })
155
+
148
156
  // Config file documentation (shown in help)
149
157
  .epilogue(
150
158
  `Examples:
@@ -180,6 +188,9 @@ Config options:
180
188
  .help("help")
181
189
  .alias("help", "h")
182
190
 
191
+ // Strict mode - reject unknown options (but allow positional args)
192
+ .strictOptions()
193
+
183
194
  // Fail handler for unknown options
184
195
  .fail((msg, err, yargsInstance) => {
185
196
  if (err) throw err;
@@ -253,6 +264,7 @@ class ArgParser {
253
264
  pioInstall: parsed["pio-install"],
254
265
  pioUninstall: parsed["pio-uninstall"],
255
266
  debugMode: parsed.debug,
267
+ serveMode: parsed.serve,
256
268
  };
257
269
  }
258
270
  }
@@ -12,6 +12,45 @@ import PathResolver from "../transpiler/data/PathResolver";
12
12
  * Command to clean generated output files
13
13
  */
14
14
  class CleanCommand {
15
+ /**
16
+ * Discover CNX files from inputs.
17
+ * Returns null if none found or on error.
18
+ */
19
+ private static discoverCnxFiles(inputs: string[]): string[] | null {
20
+ try {
21
+ const cnxFiles = InputExpansion.expandInputs(inputs);
22
+ if (cnxFiles.length === 0) {
23
+ console.log("No .cnx files found. Nothing to clean.");
24
+ return null;
25
+ }
26
+ return cnxFiles;
27
+ } catch (error) {
28
+ console.error(`Error: ${error}`);
29
+ return null;
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Delete generated files for a given set of extensions.
35
+ */
36
+ private static deleteGeneratedFiles(
37
+ baseName: string,
38
+ relativePath: string | null,
39
+ targetDir: string,
40
+ extensions: string[],
41
+ ): number {
42
+ let count = 0;
43
+ for (const ext of extensions) {
44
+ const outputPath = relativePath
45
+ ? join(targetDir, relativePath.replace(/\.cnx$|\.cnext$/, ext))
46
+ : join(targetDir, baseName + ext);
47
+ if (this.deleteIfExists(outputPath)) {
48
+ count++;
49
+ }
50
+ }
51
+ return count;
52
+ }
53
+
15
54
  /**
16
55
  * Execute the clean command
17
56
  *
@@ -24,32 +63,19 @@ class CleanCommand {
24
63
  outDir: string,
25
64
  headerOutDir?: string,
26
65
  ): void {
27
- // If no outDir specified, we can't determine where to clean
28
66
  if (!outDir) {
29
67
  console.log("No output directory specified. Nothing to clean.");
30
68
  return;
31
69
  }
32
70
 
33
- // Discover all .cnx files
34
- let cnxFiles: string[];
35
- try {
36
- cnxFiles = InputExpansion.expandInputs(inputs);
37
- } catch (error) {
38
- console.error(`Error: ${error}`);
39
- return;
40
- }
41
-
42
- if (cnxFiles.length === 0) {
43
- console.log("No .cnx files found. Nothing to clean.");
44
- return;
45
- }
71
+ const cnxFiles = this.discoverCnxFiles(inputs);
72
+ if (!cnxFiles) return;
46
73
 
47
74
  const resolvedOutDir = resolve(outDir);
48
75
  const resolvedHeaderDir = headerOutDir
49
76
  ? resolve(headerOutDir)
50
77
  : resolvedOutDir;
51
78
 
52
- // Issue #586: Use PathResolver for consistent path calculation
53
79
  const pathResolver = new PathResolver({
54
80
  inputs,
55
81
  outDir,
@@ -58,39 +84,25 @@ class CleanCommand {
58
84
 
59
85
  let deletedCount = 0;
60
86
 
61
- // For each .cnx file, calculate and delete generated files
62
87
  for (const cnxFile of cnxFiles) {
63
88
  const baseName = basename(cnxFile).replace(/\.cnx$|\.cnext$/, "");
64
-
65
- // Calculate relative path from input directories
66
89
  const relativePath = pathResolver.getRelativePathFromInputs(cnxFile);
67
90
 
68
- // Code files (.c and .cpp) go to outDir
69
- const codeExtensions = [".c", ".cpp"];
70
- for (const ext of codeExtensions) {
71
- const outputPath = relativePath
72
- ? join(resolvedOutDir, relativePath.replace(/\.cnx$|\.cnext$/, ext))
73
- : join(resolvedOutDir, baseName + ext);
74
-
75
- if (this.deleteIfExists(outputPath)) {
76
- deletedCount++;
77
- }
78
- }
79
-
80
- // Header files (.h and .hpp) go to headerOutDir
81
- const headerExtensions = [".h", ".hpp"];
82
- for (const ext of headerExtensions) {
83
- const headerPath = relativePath
84
- ? join(
85
- resolvedHeaderDir,
86
- relativePath.replace(/\.cnx$|\.cnext$/, ext),
87
- )
88
- : join(resolvedHeaderDir, baseName + ext);
89
-
90
- if (this.deleteIfExists(headerPath)) {
91
- deletedCount++;
92
- }
93
- }
91
+ // Delete code files (.c and .cpp)
92
+ deletedCount += this.deleteGeneratedFiles(
93
+ baseName,
94
+ relativePath,
95
+ resolvedOutDir,
96
+ [".c", ".cpp"],
97
+ );
98
+
99
+ // Delete header files (.h and .hpp)
100
+ deletedCount += this.deleteGeneratedFiles(
101
+ baseName,
102
+ relativePath,
103
+ resolvedHeaderDir,
104
+ [".h", ".hpp"],
105
+ );
94
106
  }
95
107
 
96
108
  if (deletedCount === 0) {
package/src/cli/Cli.ts CHANGED
@@ -38,6 +38,16 @@ class Cli {
38
38
  return { shouldRun: false, exitCode: 0 };
39
39
  }
40
40
 
41
+ // Early exit for serve mode (JSON-RPC server)
42
+ if (args.serveMode) {
43
+ return {
44
+ shouldRun: false,
45
+ exitCode: 0,
46
+ serveMode: true,
47
+ serveDebug: args.verbose,
48
+ };
49
+ }
50
+
41
51
  // Load config file (searches up from input file directory)
42
52
  const configDir =
43
53
  args.inputFiles.length > 0
@@ -6,6 +6,31 @@
6
6
  import { resolve } from "node:path";
7
7
  import { existsSync, readFileSync, writeFileSync, unlinkSync } from "node:fs";
8
8
 
9
+ /**
10
+ * Resolved paths for PlatformIO project.
11
+ */
12
+ interface IPioProjectPaths {
13
+ pioIniPath: string;
14
+ scriptPath: string;
15
+ }
16
+
17
+ /**
18
+ * Get PlatformIO project paths and validate that platformio.ini exists.
19
+ * Exits with error if not in a PlatformIO project directory.
20
+ */
21
+ function getPioProjectPaths(): IPioProjectPaths {
22
+ const pioIniPath = resolve(process.cwd(), "platformio.ini");
23
+ const scriptPath = resolve(process.cwd(), "cnext_build.py");
24
+
25
+ if (!existsSync(pioIniPath)) {
26
+ console.error("Error: platformio.ini not found in current directory");
27
+ console.error("Run this command from your PlatformIO project root");
28
+ process.exit(1);
29
+ }
30
+
31
+ return { pioIniPath, scriptPath };
32
+ }
33
+
9
34
  /**
10
35
  * PlatformIO integration commands
11
36
  */
@@ -15,15 +40,7 @@ class PlatformIOCommand {
15
40
  * Creates cnext_build.py and modifies platformio.ini
16
41
  */
17
42
  static install(): void {
18
- const pioIniPath = resolve(process.cwd(), "platformio.ini");
19
- const scriptPath = resolve(process.cwd(), "cnext_build.py");
20
-
21
- // Check if platformio.ini exists
22
- if (!existsSync(pioIniPath)) {
23
- console.error("Error: platformio.ini not found in current directory");
24
- console.error("Run this command from your PlatformIO project root");
25
- process.exit(1);
26
- }
43
+ const { pioIniPath, scriptPath } = getPioProjectPaths();
27
44
 
28
45
  // Create cnext_build.py script
29
46
  const buildScript = `Import("env")
@@ -110,15 +127,7 @@ env.AddPreAction("buildprog", transpile_cnext)
110
127
  * Deletes cnext_build.py and removes extra_scripts from platformio.ini
111
128
  */
112
129
  static uninstall(): void {
113
- const pioIniPath = resolve(process.cwd(), "platformio.ini");
114
- const scriptPath = resolve(process.cwd(), "cnext_build.py");
115
-
116
- // Check if platformio.ini exists
117
- if (!existsSync(pioIniPath)) {
118
- console.error("Error: platformio.ini not found in current directory");
119
- console.error("Run this command from your PlatformIO project root");
120
- process.exit(1);
121
- }
130
+ const { pioIniPath, scriptPath } = getPioProjectPaths();
122
131
 
123
132
  let hasChanges = false;
124
133
 
package/src/cli/Runner.ts CHANGED
@@ -9,6 +9,19 @@ import InputExpansion from "../transpiler/data/InputExpansion";
9
9
  import Transpiler from "../transpiler/Transpiler";
10
10
  import ICliConfig from "./types/ICliConfig";
11
11
  import ResultPrinter from "./ResultPrinter";
12
+ 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
+ }
19
+
20
+ /** Result of determining output path */
21
+ interface IOutputPathResult {
22
+ outDir: string;
23
+ explicitOutputFile: string | null;
24
+ }
12
25
 
13
26
  /**
14
27
  * Execute the transpiler
@@ -19,24 +32,63 @@ class Runner {
19
32
  * @param config - CLI configuration
20
33
  */
21
34
  static async execute(config: ICliConfig): Promise<void> {
22
- // Identify which inputs are directories (for structure preservation)
23
- // These will be passed to Transpiler so it can preserve directory structure
35
+ const { srcDirs, explicitFiles } = this._categorizeInputs(config.inputs);
36
+ const files = this._expandInputFiles(config.inputs);
37
+ const { outDir, explicitOutputFile } = this._determineOutputPath(
38
+ config,
39
+ files,
40
+ );
41
+
42
+ const pipeline = new Transpiler({
43
+ inputs: [...srcDirs, ...explicitFiles],
44
+ includeDirs: config.includeDirs,
45
+ outDir,
46
+ headerOutDir: config.headerOutDir,
47
+ basePath: config.basePath,
48
+ preprocess: config.preprocess,
49
+ defines: config.defines,
50
+ cppRequired: config.cppRequired,
51
+ noCache: config.noCache,
52
+ parseOnly: config.parseOnly,
53
+ target: config.target,
54
+ debugMode: config.debugMode,
55
+ });
56
+
57
+ const result = await pipeline.run();
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 {
24
68
  const srcDirs: string[] = [];
25
69
  const explicitFiles: string[] = [];
26
70
 
27
- for (const input of config.inputs) {
71
+ for (const input of inputs) {
28
72
  const resolvedPath = resolve(input);
29
- if (existsSync(resolvedPath) && statSync(resolvedPath).isDirectory()) {
73
+ const isDir =
74
+ existsSync(resolvedPath) && statSync(resolvedPath).isDirectory();
75
+ if (isDir) {
30
76
  srcDirs.push(resolvedPath);
31
77
  } else {
32
78
  explicitFiles.push(resolvedPath);
33
79
  }
34
80
  }
35
81
 
36
- // Step 1: Expand directories to .cnx files
82
+ return { srcDirs, explicitFiles };
83
+ }
84
+
85
+ /**
86
+ * Expand input paths to .cnx files.
87
+ */
88
+ private static _expandInputFiles(inputs: string[]): string[] {
37
89
  let files: string[];
38
90
  try {
39
- files = InputExpansion.expandInputs(config.inputs);
91
+ files = InputExpansion.expandInputs(inputs);
40
92
  } catch (error) {
41
93
  console.error(`Error: ${error}`);
42
94
  process.exit(1);
@@ -47,77 +99,71 @@ class Runner {
47
99
  process.exit(1);
48
100
  }
49
101
 
50
- // Step 2: Determine output directory and explicit filename
51
- // Note: Include path auto-discovery happens inside Transpiler.discoverSources()
52
- let outDir: string;
53
- let explicitOutputFile: string | null = null;
102
+ return files;
103
+ }
54
104
 
55
- // Check if outputPath is an explicit file (ends with .c or .cpp)
56
- const isExplicitFile =
57
- config.outputPath &&
58
- /\.(c|cpp)$/.test(config.outputPath) &&
59
- !config.outputPath.endsWith("/");
60
-
61
- if (config.outputPath) {
62
- // User specified -o
63
- const stats = existsSync(config.outputPath)
64
- ? statSync(config.outputPath)
65
- : null;
66
- if (stats?.isDirectory() || config.outputPath.endsWith("/")) {
67
- outDir = config.outputPath;
68
- } else if (isExplicitFile) {
69
- // Explicit output file path
70
- if (files.length > 1) {
71
- console.error(
72
- "Error: Cannot use explicit output filename with multiple input files",
73
- );
74
- console.error("Use a directory path instead: -o <directory>/");
75
- process.exit(1);
76
- }
77
- outDir = dirname(config.outputPath);
78
- explicitOutputFile = resolve(config.outputPath);
79
- } else {
80
- outDir = config.outputPath;
81
- }
82
- } else {
83
- // No -o flag: use same directory as first input file
84
- outDir = dirname(files[0]);
105
+ /**
106
+ * Determine output directory and explicit filename from config.
107
+ */
108
+ private static _determineOutputPath(
109
+ config: ICliConfig,
110
+ files: string[],
111
+ ): IOutputPathResult {
112
+ if (!config.outputPath) {
113
+ return { outDir: dirname(files[0]), explicitOutputFile: null };
85
114
  }
86
115
 
87
- // Step 3: Create Transpiler
88
- // Combine srcDirs and explicitFiles into inputs - Transpiler handles both
89
- const pipelineInputs = [...srcDirs, ...explicitFiles];
90
- const pipeline = new Transpiler({
91
- inputs: pipelineInputs,
92
- includeDirs: config.includeDirs,
93
- outDir,
94
- headerOutDir: config.headerOutDir,
95
- basePath: config.basePath,
96
- preprocess: config.preprocess,
97
- defines: config.defines,
98
- cppRequired: config.cppRequired,
99
- noCache: config.noCache,
100
- parseOnly: config.parseOnly,
101
- target: config.target,
102
- debugMode: config.debugMode,
103
- });
116
+ const isExplicitFile =
117
+ /\.(c|cpp)$/.test(config.outputPath) && !config.outputPath.endsWith("/");
104
118
 
105
- // Step 4: Compile
106
- const result = await pipeline.run();
119
+ const stats = existsSync(config.outputPath)
120
+ ? statSync(config.outputPath)
121
+ : null;
107
122
 
108
- // Step 5: Rename output file if explicit filename was specified
109
- if (explicitOutputFile && result.success && result.outputFiles.length > 0) {
110
- const generatedFile = result.outputFiles[0];
111
- // Only rename if it's different from the desired path
112
- if (generatedFile !== explicitOutputFile) {
113
- renameSync(generatedFile, explicitOutputFile);
114
- // Update the result to show the correct path
115
- result.outputFiles[0] = explicitOutputFile;
123
+ // Directory path
124
+ if (stats?.isDirectory() || config.outputPath.endsWith("/")) {
125
+ return { outDir: config.outputPath, explicitOutputFile: null };
126
+ }
127
+
128
+ // Explicit output file
129
+ 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);
116
136
  }
137
+ return {
138
+ outDir: dirname(config.outputPath),
139
+ explicitOutputFile: resolve(config.outputPath),
140
+ };
117
141
  }
118
142
 
119
- ResultPrinter.print(result);
120
- process.exit(result.success ? 0 : 1);
143
+ // Default: treat as directory
144
+ return { outDir: config.outputPath, explicitOutputFile: null };
145
+ }
146
+
147
+ /**
148
+ * Rename output file if explicit filename was specified.
149
+ */
150
+ private static _renameOutputIfNeeded(
151
+ result: ITranspilerResult,
152
+ explicitOutputFile: string | null,
153
+ ): void {
154
+ if (
155
+ !explicitOutputFile ||
156
+ !result.success ||
157
+ result.outputFiles.length === 0
158
+ ) {
159
+ return;
160
+ }
161
+
162
+ const generatedFile = result.outputFiles[0];
163
+ if (generatedFile !== explicitOutputFile) {
164
+ renameSync(generatedFile, explicitOutputFile);
165
+ result.outputFiles[0] = explicitOutputFile;
166
+ }
121
167
  }
122
168
  }
123
169
 
@@ -281,20 +281,24 @@ describe("ArgParser", () => {
281
281
  expect(consoleLogSpy).toHaveBeenCalled();
282
282
  });
283
283
 
284
- it("ignores -I flag (yargs without strict mode)", () => {
285
- // yargs without strict mode accepts unknown options
286
- // The fail handler only triggers on actual parse errors (e.g., missing requiresArg values)
287
- // In practice, users will see -I being ignored and the help text shows --include
288
- const result = ArgParser.parse(argv("input.cnx", "-I", "path"));
289
- expect(result.inputFiles).toEqual(["input.cnx"]);
290
- // -I doesn't populate includeDirs - shows users to use --include
291
- expect(result.includeDirs).toEqual([]);
284
+ it("shows helpful message for -I flag (GCC-style)", () => {
285
+ // Use "-I path" with a space (yargs parses "-I<dir>" as multiple short flags)
286
+ expect(() => ArgParser.parse(argv("input.cnx", "-I", "path"))).toThrow(
287
+ "process.exit(1)",
288
+ );
289
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
290
+ "Error: Unknown flag '-I...'",
291
+ );
292
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
293
+ " Did you mean: --include <dir>",
294
+ );
292
295
  });
293
296
 
294
- it("ignores unknown flags (yargs without strict mode)", () => {
295
- // yargs without strict mode accepts unknown options
296
- const result = ArgParser.parse(argv("input.cnx", "--unknown-flag"));
297
- expect(result.inputFiles).toEqual(["input.cnx"]);
297
+ it("rejects unknown flags with error", () => {
298
+ expect(() =>
299
+ ArgParser.parse(argv("input.cnx", "--unknown-flag")),
300
+ ).toThrow("process.exit(1)");
301
+ expect(exitSpy).toHaveBeenCalledWith(1);
298
302
  });
299
303
  });
300
304
  });
@@ -50,6 +50,7 @@ describe("Cli", () => {
50
50
  pioInstall: false,
51
51
  pioUninstall: false,
52
52
  debugMode: false,
53
+ serveMode: false,
53
54
  };
54
55
 
55
56
  // Default mocks
@@ -95,6 +96,17 @@ describe("Cli", () => {
95
96
  expect(result.exitCode).toBe(0);
96
97
  });
97
98
 
99
+ it("handles --serve flag", () => {
100
+ mockParsedArgs.serveMode = true;
101
+ vi.mocked(ArgParser.parse).mockReturnValue(mockParsedArgs);
102
+
103
+ const result = Cli.run();
104
+
105
+ expect(result.shouldRun).toBe(false);
106
+ expect(result.exitCode).toBe(0);
107
+ expect(result.serveMode).toBe(true);
108
+ });
109
+
98
110
  it("handles --config flag", () => {
99
111
  mockParsedArgs.showConfig = true;
100
112
  vi.mocked(ArgParser.parse).mockReturnValue(mockParsedArgs);
@@ -294,6 +306,7 @@ describe("Cli", () => {
294
306
  showConfig: false,
295
307
  pioInstall: false,
296
308
  pioUninstall: false,
309
+ serveMode: false,
297
310
  };
298
311
  vi.mocked(ArgParser.parse).mockReturnValue(mockParsedArgs);
299
312