@typecad/expect 1.0.0-alpha.6 → 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.
@@ -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,8 +7,12 @@
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';
11
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);
12
16
  /** Format a check failure into the `error` field used by CompileResult/UploadResult. */
13
17
  function formatEnvFailure(failure) {
14
18
  const lines = [...failure.messages];
@@ -23,12 +27,35 @@ function formatEnvFailure(failure) {
23
27
  * transpiler, and returns the path to the generated .ino file.
24
28
  */
25
29
  export function transpileTestFile(preprocessedSource, originalFilePath, projectRoot, buildTarget) {
26
- // 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).
27
36
  const baseName = path.basename(originalFilePath, '.test.ts').replace(/[^a-zA-Z0-9_]/g, '_');
28
- 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);
29
41
  try {
30
- fs.rmSync(buildDir, { recursive: true, force: true });
31
- 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
+ }
32
59
  }
33
60
  catch {
34
61
  return { success: false, sketchDir: buildDir, sketchPath: '', output: '', error: `Failed to create build dir: ${buildDir}` };
@@ -82,9 +109,27 @@ export function transpileTestFile(preprocessedSource, originalFilePath, projectR
82
109
  };
83
110
  }
84
111
  /**
85
- * 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
86
115
  */
87
- 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) {
88
133
  // Hard gate: verify arduino-cli + core before spawning.
89
134
  {
90
135
  const gate = checkArduinoEnv(buildTarget);
@@ -106,7 +151,68 @@ export function compileSketch(sketchDir, buildTarget) {
106
151
  /**
107
152
  * Upload the compiled sketch to the board.
108
153
  */
109
- 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) {
110
216
  // Hard gate: verify arduino-cli + core before spawning.
111
217
  {
112
218
  const gate = checkArduinoEnv(buildTarget);
@@ -174,13 +280,14 @@ function findOutputDir(buildDir, baseName, projectRoot) {
174
280
  function hasInoFile(dir) {
175
281
  if (!fs.existsSync(dir))
176
282
  return false;
177
- return fs.readdirSync(dir).some(f => f.endsWith('.ino'));
283
+ return fs.readdirSync(dir).some(f => f.endsWith('.ino') || f.endsWith('.cc'));
178
284
  }
179
285
  function findInoFile(dir) {
180
286
  if (!fs.existsSync(dir))
181
287
  return undefined;
182
288
  for (const entry of fs.readdirSync(dir)) {
183
- 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')) {
184
291
  return path.join(dir, entry);
185
292
  }
186
293
  }
@@ -190,7 +297,7 @@ function findInoFileRecursive(dir) {
190
297
  if (!fs.existsSync(dir))
191
298
  return undefined;
192
299
  for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
193
- if (entry.name.endsWith('.ino')) {
300
+ if (entry.name.endsWith('.ino') || entry.name.endsWith('.cc')) {
194
301
  return path.join(dir, entry.name);
195
302
  }
196
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.6",
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",
@@ -40,10 +40,10 @@
40
40
  "dependencies": {
41
41
  "typescript": "^5.7.3",
42
42
  "serialport": "^12.0.0",
43
- "@typecad/arduino-cli": "1.0.0-alpha.6"
43
+ "@typecad/arduino-cli": "1.0.0-alpha.7"
44
44
  },
45
45
  "devDependencies": {
46
- "@typecad/cuttlefish": "1.0.0-alpha.6",
46
+ "@typecad/cuttlefish": "1.0.0-alpha.7",
47
47
  "@types/node": "^22.10.7"
48
48
  },
49
49
  "license": "MIT",