@typecad/expect 1.0.0-alpha.3 → 1.0.0-alpha.7

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.
package/README.md CHANGED
@@ -152,13 +152,13 @@ All numeric matchers return the parent `Suite`, so you can continue the chain wi
152
152
 
153
153
  ```bash
154
154
  # Run a specific file
155
- npx cuttlefish-test examples/my-sensor.test.ts
155
+ npx --package=@typecad/expect cuttlefish-test examples/my-sensor.test.ts
156
156
 
157
157
  # Run all test files matched by config include patterns
158
- npx cuttlefish-test
158
+ npx --package=@typecad/expect cuttlefish-test
159
159
 
160
160
  # Override the port at run-time
161
- npx cuttlefish-test --port /dev/ttyACM0 examples/my-sensor.test.ts
161
+ npx --package=@typecad/expect cuttlefish-test --port /dev/ttyACM0 examples/my-sensor.test.ts
162
162
  ```
163
163
 
164
164
  Or via the npm script defined in the root `package.json`:
@@ -253,10 +253,10 @@ Example flow:
253
253
 
254
254
  ```bash
255
255
  # 1. Compile and upload the serial-output showcase
256
- npx cuttlefish src/23-transpiler-showcase.ts --compile --upload --port COM4
256
+ npx @typecad/cuttlefish src/23-transpiler-showcase.ts --compile --upload --port COM4
257
257
 
258
258
  # 2. Run the on-hardware expect test against the connected Uno
259
- npx cuttlefish-test examples/24-uno-validation.test.ts --port COM4
259
+ npx --package=@typecad/expect cuttlefish-test examples/24-uno-validation.test.ts --port COM4
260
260
  ```
261
261
 
262
262
  This hybrid workflow is the recommended way to confirm that simple variables, arithmetic, arrays, enums, functions, GPIO, and analog input are behaving correctly on real Uno hardware.
@@ -18,10 +18,17 @@ export interface UploadResult {
18
18
  */
19
19
  export declare function transpileTestFile(preprocessedSource: string, originalFilePath: string, projectRoot: string, buildTarget: string): CompileResult;
20
20
  /**
21
- * Compile the Arduino sketch using arduino-cli.
21
+ * Compile the sketch. Detects the toolchain from the framework:
22
+ * - framework-esp32 → idf.py build (via framework-esp32's compileEspIdf)
23
+ * - everything else → arduino-cli compile
22
24
  */
23
- export declare function compileSketch(sketchDir: string, buildTarget: string): CompileResult;
25
+ export declare function compileSketch(sketchDir: string, buildTarget: string, framework?: string): CompileResult;
24
26
  /**
25
27
  * Upload the compiled sketch to the board.
26
28
  */
27
- export declare function uploadSketch(sketchDir: string, buildTarget: string, port: string): UploadResult;
29
+ /**
30
+ * Upload firmware. Detects the toolchain from the framework:
31
+ * - framework-esp32 → idf.py flash
32
+ * - everything else → arduino-cli upload
33
+ */
34
+ export declare function uploadSketch(sketchDir: string, buildTarget: string, port: string, framework?: string): UploadResult;
@@ -7,7 +7,19 @@
7
7
  import path from 'node:path';
8
8
  import fs from 'node:fs';
9
9
  import { spawnSync } from 'node:child_process';
10
+ import { createRequire } from 'node:module';
10
11
  import { parseConfigAST } from './config.js';
12
+ import { checkArduinoEnv } from '@typecad/arduino-cli';
13
+ // createRequire lets us synchronously require CommonJS modules from this ESM
14
+ // file. Used to load framework-esp32's compiled dist at runtime.
15
+ const require_ = createRequire(import.meta.url);
16
+ /** Format a check failure into the `error` field used by CompileResult/UploadResult. */
17
+ function formatEnvFailure(failure) {
18
+ const lines = [...failure.messages];
19
+ if (failure.fixCommand)
20
+ lines.push(` Fix: ${failure.fixCommand}`);
21
+ return lines.join('\n');
22
+ }
11
23
  /**
12
24
  * Transpile preprocessed TypeScript source to a C++ Arduino sketch.
13
25
  *
@@ -15,12 +27,35 @@ import { parseConfigAST } from './config.js';
15
27
  * transpiler, and returns the path to the generated .ino file.
16
28
  */
17
29
  export function transpileTestFile(preprocessedSource, originalFilePath, projectRoot, buildTarget) {
18
- // Create a build directory for this test file
30
+ // Create a build directory for this test file.
31
+ // For idf.py targets, use a SHARED build directory across all test files —
32
+ // only main/main.cc changes between files, so idf.py can do incremental
33
+ // builds (ninja detects only the changed .cc and relinks). This turns the
34
+ // 2nd+ file from a ~130s full rebuild into a ~10-15s incremental build.
35
+ // For arduino-cli targets, use per-file directories (no incremental benefit).
19
36
  const baseName = path.basename(originalFilePath, '.test.ts').replace(/[^a-zA-Z0-9_]/g, '_');
20
- const buildDir = path.join(projectRoot, '.build', 'expect', baseName);
37
+ const isEsp32 = buildTarget === 'esp32' || buildTarget === 'esp32s3' || buildTarget === 'esp32c3' || buildTarget === 'esp32c6';
38
+ const buildDir = isEsp32
39
+ ? path.join(projectRoot, '.build', 'expect', 'esp32_shared')
40
+ : path.join(projectRoot, '.build', 'expect', baseName);
21
41
  try {
22
- fs.rmSync(buildDir, { recursive: true, force: true });
23
- fs.mkdirSync(buildDir, { recursive: true });
42
+ if (isEsp32 && fs.existsSync(buildDir)) {
43
+ // Shared ESP32 build dir: preserve it. The transpiler will overwrite
44
+ // the .ts source and regenerate main.cc; ninja does an incremental build.
45
+ // Only remove the old .ts file so the transpiler doesn't pick up stale ones.
46
+ const oldTsFiles = fs.readdirSync(buildDir).filter(f => f.endsWith('.ts'));
47
+ for (const f of oldTsFiles) {
48
+ try {
49
+ fs.unlinkSync(path.join(buildDir, f));
50
+ }
51
+ catch { }
52
+ }
53
+ }
54
+ else {
55
+ // Fresh dir (first run, or arduino-cli target).
56
+ fs.rmSync(buildDir, { recursive: true, force: true });
57
+ fs.mkdirSync(buildDir, { recursive: true });
58
+ }
24
59
  }
25
60
  catch {
26
61
  return { success: false, sketchDir: buildDir, sketchPath: '', output: '', error: `Failed to create build dir: ${buildDir}` };
@@ -74,9 +109,35 @@ export function transpileTestFile(preprocessedSource, originalFilePath, projectR
74
109
  };
75
110
  }
76
111
  /**
77
- * Compile the Arduino sketch using arduino-cli.
112
+ * Compile the sketch. Detects the toolchain from the framework:
113
+ * - framework-esp32 → idf.py build (via framework-esp32's compileEspIdf)
114
+ * - everything else → arduino-cli compile
78
115
  */
79
- export function compileSketch(sketchDir, buildTarget) {
116
+ export function compileSketch(sketchDir, buildTarget, framework) {
117
+ if (framework?.includes('framework-esp32')) {
118
+ return compileEspIdfSketch(sketchDir, buildTarget);
119
+ }
120
+ return compileArduinoSketch(sketchDir, buildTarget);
121
+ }
122
+ /** Resolve the framework-esp32 dist/toolchain directory from node_modules.
123
+ * Uses the main export entry point (dist/index.js) to find the dist dir,
124
+ * avoiding the package's exports field restriction on subpaths. */
125
+ function resolveFrameworkEsp32Dist() {
126
+ const indexPath = require_.resolve('@typecad/framework-esp32');
127
+ // indexPath = .../packages/framework-esp32/dist/index.js
128
+ // toolchain dir = .../packages/framework-esp32/dist/toolchain
129
+ return path.join(path.dirname(indexPath), 'toolchain');
130
+ }
131
+ /** Compile via arduino-cli (framework-arduino / framework-avr). */
132
+ function compileArduinoSketch(sketchDir, buildTarget) {
133
+ // Hard gate: verify arduino-cli + core before spawning.
134
+ {
135
+ const gate = checkArduinoEnv(buildTarget);
136
+ if (!gate.ok) {
137
+ const message = formatEnvFailure(gate);
138
+ return { success: false, sketchDir, sketchPath: '', output: message, error: message };
139
+ }
140
+ }
80
141
  const result = spawnSync('arduino-cli', ['compile', '--fqbn', buildTarget, sketchDir], { encoding: 'utf8', timeout: 120000 });
81
142
  const output = `${result.stdout ?? ''}\n${result.stderr ?? ''}`.trim();
82
143
  return {
@@ -90,7 +151,76 @@ export function compileSketch(sketchDir, buildTarget) {
90
151
  /**
91
152
  * Upload the compiled sketch to the board.
92
153
  */
93
- export function uploadSketch(sketchDir, buildTarget, port) {
154
+ /**
155
+ * Upload firmware. Detects the toolchain from the framework:
156
+ * - framework-esp32 → idf.py flash
157
+ * - everything else → arduino-cli upload
158
+ */
159
+ export function uploadSketch(sketchDir, buildTarget, port, framework) {
160
+ if (framework?.includes('framework-esp32')) {
161
+ return uploadEspIdfSketch(sketchDir, buildTarget, port);
162
+ }
163
+ return uploadArduinoSketch(sketchDir, buildTarget, port);
164
+ }
165
+ /** Compile via idf.py build (framework-esp32). */
166
+ function compileEspIdfSketch(sketchDir, buildTarget) {
167
+ try {
168
+ const projectDir = path.basename(sketchDir) === 'main' ? path.dirname(sketchDir) : sketchDir;
169
+ const distDir = resolveFrameworkEsp32Dist();
170
+ const { compileEspIdf } = require_(path.join(distDir, 'compile.js'));
171
+ const result = compileEspIdf({
172
+ sourcePath: projectDir,
173
+ target: buildTarget || 'esp32',
174
+ });
175
+ return {
176
+ success: result.success,
177
+ sketchDir: projectDir,
178
+ sketchPath: '',
179
+ output: result.output,
180
+ error: result.errorMessage,
181
+ };
182
+ }
183
+ catch (e) {
184
+ return {
185
+ success: false,
186
+ sketchDir,
187
+ sketchPath: '',
188
+ output: '',
189
+ error: `idf.py compile failed: ${e instanceof Error ? e.message : String(e)}`,
190
+ };
191
+ }
192
+ }
193
+ /** Upload via idf.py flash (framework-esp32). */
194
+ function uploadEspIdfSketch(sketchDir, buildTarget, port) {
195
+ try {
196
+ const projectDir = path.basename(sketchDir) === 'main' ? path.dirname(sketchDir) : sketchDir;
197
+ const distDir = resolveFrameworkEsp32Dist();
198
+ const { uploadEspIdf } = require_(path.join(distDir, 'upload.js'));
199
+ const result = uploadEspIdf(projectDir, port);
200
+ return {
201
+ success: result.success,
202
+ output: result.output,
203
+ error: result.errorMessage,
204
+ };
205
+ }
206
+ catch (e) {
207
+ return {
208
+ success: false,
209
+ output: '',
210
+ error: `idf.py flash failed: ${e instanceof Error ? e.message : String(e)}`,
211
+ };
212
+ }
213
+ }
214
+ /** Upload via arduino-cli (framework-arduino / framework-avr). */
215
+ function uploadArduinoSketch(sketchDir, buildTarget, port) {
216
+ // Hard gate: verify arduino-cli + core before spawning.
217
+ {
218
+ const gate = checkArduinoEnv(buildTarget);
219
+ if (!gate.ok) {
220
+ const message = formatEnvFailure(gate);
221
+ return { success: false, output: message, error: message };
222
+ }
223
+ }
94
224
  const result = spawnSync('arduino-cli', ['upload', '--fqbn', buildTarget, '--port', port, sketchDir], { encoding: 'utf8', timeout: 60000 });
95
225
  const output = `${result.stdout ?? ''}\n${result.stderr ?? ''}`.trim();
96
226
  return {
@@ -150,13 +280,14 @@ function findOutputDir(buildDir, baseName, projectRoot) {
150
280
  function hasInoFile(dir) {
151
281
  if (!fs.existsSync(dir))
152
282
  return false;
153
- return fs.readdirSync(dir).some(f => f.endsWith('.ino'));
283
+ return fs.readdirSync(dir).some(f => f.endsWith('.ino') || f.endsWith('.cc'));
154
284
  }
155
285
  function findInoFile(dir) {
156
286
  if (!fs.existsSync(dir))
157
287
  return undefined;
158
288
  for (const entry of fs.readdirSync(dir)) {
159
- if (entry.endsWith('.ino')) {
289
+ // Accept .ino (Arduino) and .cc (ESP-IDF main.cc) as entry files.
290
+ if (entry.endsWith('.ino') || entry.endsWith('.cc')) {
160
291
  return path.join(dir, entry);
161
292
  }
162
293
  }
@@ -166,7 +297,7 @@ function findInoFileRecursive(dir) {
166
297
  if (!fs.existsSync(dir))
167
298
  return undefined;
168
299
  for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
169
- if (entry.name.endsWith('.ino')) {
300
+ if (entry.name.endsWith('.ino') || entry.name.endsWith('.cc')) {
170
301
  return path.join(dir, entry.name);
171
302
  }
172
303
  if (entry.isDirectory()) {
@@ -17,6 +17,13 @@ export interface OutputShim {
17
17
  export declare const serialShim: OutputShim;
18
18
  /** AVR native UART shim: routes through framework-avr's _uart_* helpers. */
19
19
  export declare const avrUartShim: OutputShim;
20
+ /** ESP-IDF shim: emits console.debug (print, no newline) and console.log
21
+ * (println, with newline) calls. The transpiler's transformConsoleCall lowers
22
+ * these to printf for framework-esp32. Using different console methods lets
23
+ * the lowering distinguish "partial line" (print/debug) from "end of line"
24
+ * (println/log) — critical for the [TC:EXPECT:...] protocol format which
25
+ * spans multiple print calls on a single line. */
26
+ export declare const espIdfShim: OutputShim;
20
27
  export interface PreprocessorOptions {
21
28
  /** Wrap string literals in Arduino F() macro to save SRAM on AVR. */
22
29
  isAvr?: boolean;
@@ -38,6 +38,18 @@ export const avrUartShim = {
38
38
  println: (e) => `_uart_println_expr(${e})`,
39
39
  delay: '_native_delay_ms(1000)',
40
40
  };
41
+ /** ESP-IDF shim: emits console.debug (print, no newline) and console.log
42
+ * (println, with newline) calls. The transpiler's transformConsoleCall lowers
43
+ * these to printf for framework-esp32. Using different console methods lets
44
+ * the lowering distinguish "partial line" (print/debug) from "end of line"
45
+ * (println/log) — critical for the [TC:EXPECT:...] protocol format which
46
+ * spans multiple print calls on a single line. */
47
+ export const espIdfShim = {
48
+ begin: '',
49
+ print: (e) => `console.debug(${e})`,
50
+ println: (e) => `console.log(${e})`,
51
+ delay: 'Timing.delay(1000)',
52
+ };
41
53
  /**
42
54
  * Preprocess a test file's TypeScript source.
43
55
  *
@@ -7,7 +7,7 @@
7
7
  import fs from 'node:fs';
8
8
  import path from 'node:path';
9
9
  import { findTestFiles } from './finder.js';
10
- import { preprocess, serialShim, avrUartShim } from './preprocessor.js';
10
+ import { preprocess, serialShim, avrUartShim, espIdfShim } from './preprocessor.js';
11
11
  import { transpileTestFile, compileSketch, uploadSketch } from './compiler.js';
12
12
  import { readSerialOutput } from './serial.js';
13
13
  import { parseProtocolLines } from './parser.js';
@@ -84,7 +84,9 @@ async function processTestFile(filePath, config) {
84
84
  try {
85
85
  preprocessed = preprocess(source, path.basename(filePath), {
86
86
  isAvr: config.target === 'avr' || config.target === 'megaavr',
87
- shim: config.framework === '@typecad/framework-avr' ? avrUartShim : serialShim,
87
+ shim: config.framework === '@typecad/framework-avr' ? avrUartShim
88
+ : config.framework?.includes('framework-esp32') ? espIdfShim
89
+ : serialShim,
88
90
  });
89
91
  }
90
92
  catch (e) {
@@ -96,15 +98,15 @@ async function processTestFile(filePath, config) {
96
98
  if (!transpileResult.success) {
97
99
  return errorResult(filePath, transpileResult.error ?? 'Transpilation failed', startTime);
98
100
  }
99
- // Step 3: Compile with arduino-cli
101
+ // Step 3: Compile (arduino-cli or idf.py depending on framework)
100
102
  console.log(` ${DIM}compiling...${RESET}`);
101
- const compileResult = compileSketch(transpileResult.sketchDir, config.buildTarget);
103
+ const compileResult = compileSketch(transpileResult.sketchDir, config.buildTarget, config.framework);
102
104
  if (!compileResult.success) {
103
105
  return errorResult(filePath, compileResult.error ?? 'Compilation failed', startTime);
104
106
  }
105
- // Step 4: Upload
107
+ // Step 4: Upload (arduino-cli or idf.py depending on framework)
106
108
  console.log(` ${DIM}uploading to ${config.test.port}...${RESET}`);
107
- const uploadResult = uploadSketch(transpileResult.sketchDir, config.buildTarget, config.test.port);
109
+ const uploadResult = uploadSketch(transpileResult.sketchDir, config.buildTarget, config.test.port, config.framework);
108
110
  if (!uploadResult.success) {
109
111
  return errorResult(filePath, uploadResult.error ?? 'Upload failed', startTime);
110
112
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@typecad/expect",
3
- "version": "1.0.0-alpha.3",
3
+ "version": "1.0.0-alpha.7",
4
4
  "description": "Hardware test framework for TypeCAD — vitest-style assertions over serial",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -39,10 +39,11 @@
39
39
  },
40
40
  "dependencies": {
41
41
  "typescript": "^5.7.3",
42
- "serialport": "^12.0.0"
42
+ "serialport": "^12.0.0",
43
+ "@typecad/arduino-cli": "1.0.0-alpha.7"
43
44
  },
44
45
  "devDependencies": {
45
- "@typecad/cuttlefish": "1.0.0-alpha.3",
46
+ "@typecad/cuttlefish": "1.0.0-alpha.7",
46
47
  "@types/node": "^22.10.7"
47
48
  },
48
49
  "license": "MIT",