@typecad/cuttlefish 1.0.0-alpha.11 → 1.0.0-alpha.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.
- package/dist/add-preset.d.ts +4 -0
- package/dist/add-preset.js +74 -0
- package/dist/api/config.d.ts +5 -5
- package/dist/api/shared/display-adapters/sdl.js +1 -1
- package/dist/api/shared/display-profile.d.ts +11 -0
- package/dist/api/shared/display-profile.js +3 -0
- package/dist/api/shared/framework-manifest.d.ts +85 -78
- package/dist/api/shared/framework-manifest.js +1 -0
- package/dist/api/shared/hal-op-ir.d.ts +19 -0
- package/dist/api/shared/toolchain-types.d.ts +0 -1
- package/dist/cli.js +21 -4
- package/dist/config-loader.d.ts +8 -2
- package/dist/config-loader.js +202 -53
- package/dist/config-schema.d.ts +7 -7
- package/dist/config-schema.js +3 -3
- package/dist/create/board-spec.d.ts +122 -122
- package/dist/create/debug-artifacts.d.ts +20 -0
- package/dist/create/debug-artifacts.js +69 -0
- package/dist/create/eslint-rules-template.js +6 -3
- package/dist/create/index.d.ts +2 -0
- package/dist/create/index.js +1 -0
- package/dist/create/init-scaffold.d.ts +1 -0
- package/dist/create/init-scaffold.js +5 -0
- package/dist/create/init-templates.js +0 -2
- package/dist/emit/compliance/rules.js +52 -4
- package/dist/emit/emitters/function-emitter-impl.js +7 -1
- package/dist/emit/emitters/line-appender.js +6 -0
- package/dist/emit/emitters/setup.js +15 -2
- package/dist/emit/emitters/ui-emitter.js +40 -15
- package/dist/emit/route-hal-op.js +55 -1
- package/dist/emit/statement-renderer.js +5 -2
- package/dist/ir/build-ir.js +22 -1
- package/dist/ir/expression-to-ir.js +17 -0
- package/dist/ir/feature-registry.js +22 -6
- package/dist/ir/hal/hal-emitter.js +23 -5
- package/dist/ir/hal/hal-plugins.js +11 -0
- package/dist/ir/pin-mode-validation.js +32 -9
- package/dist/ir/pin-state-tracking.d.ts +58 -0
- package/dist/ir/pin-state-tracking.js +182 -0
- package/dist/ir/program-analysis.d.ts +10 -2
- package/dist/ir/program-analysis.js +40 -4
- package/dist/ir/statement-to-ir.js +14 -0
- package/dist/ir/transformers/control-flow.js +29 -0
- package/dist/ir/transformers/ui-call-resolver.js +105 -1
- package/dist/ir/ui-element-auto-wire.js +7 -4
- package/dist/orchestrator/graph-builder.d.ts +4 -1
- package/dist/orchestrator/graph-builder.js +7 -1
- package/dist/platform/async-runtime.d.ts +1 -1
- package/dist/platform/async-runtime.js +12 -3
- package/dist/platform/generic-strategy.js +1 -1
- package/dist/preview/api-shared-shim.d.ts +1 -0
- package/dist/preview/api-shared-shim.js +7 -0
- package/dist/preview/client.js +220 -1
- package/dist/preview/server.js +154 -62
- package/dist/theme-tokens.d.ts +22 -0
- package/dist/theme-tokens.js +172 -0
- package/dist/transpile.js +90 -12
- package/dist/types.d.ts +5 -0
- package/dist/ui-hook.d.ts +7 -0
- package/dist/utils/cli.js +9 -0
- package/dist/utils/fs.d.ts +2 -0
- package/dist/utils/fs.js +16 -0
- package/dist/utils/ui.d.ts +5 -0
- package/dist/utils/ui.js +7 -0
- package/package.json +7 -5
package/dist/cli.js
CHANGED
|
@@ -3,6 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import fs from "node:fs";
|
|
4
4
|
import { parseCommandLine, printHelp } from "./utils/cli.js";
|
|
5
5
|
import { scaffoldProject, printInitNextSteps, KNOWN_TARGETS, frameworksForTarget, frameworkCatalogEntry, FRAMEWORK_CATALOG, frameworkTargetProfile } from "./create/index.js";
|
|
6
|
+
import { generateFrameworkDebugArtifacts } from "./create/debug-artifacts.js";
|
|
6
7
|
import { runInitWizard } from "./create/index.js";
|
|
7
8
|
import { installProjectDependencies } from "./create/install-deps.js";
|
|
8
9
|
import { generateLibraryDefinitions, transpileFile } from "./transpile.js";
|
|
@@ -156,7 +157,20 @@ function finalizeCreate(result, options) {
|
|
|
156
157
|
console.log(chalk.dim(` Run '${chalk.white("npm install")}' manually in ${relativeDir} when ready.`));
|
|
157
158
|
}
|
|
158
159
|
}
|
|
159
|
-
|
|
160
|
+
// Framework starter debug profile (e.g. Zephyr esp32s3): write .vscode/
|
|
161
|
+
// launch.json + tasks.json so F5 in VS Code works before the first build.
|
|
162
|
+
// Runs after the install step so the framework package resolves from the
|
|
163
|
+
// new project's node_modules; best-effort — the first --debug build writes
|
|
164
|
+
// the artifacts anyway.
|
|
165
|
+
const debugArtifacts = generateFrameworkDebugArtifacts({
|
|
166
|
+
frameworkPackage: result.options.frameworkPackage || undefined,
|
|
167
|
+
workspaceRoot: result.outDir,
|
|
168
|
+
buildTarget: result.options.buildTarget,
|
|
169
|
+
});
|
|
170
|
+
if (debugArtifacts.length > 0) {
|
|
171
|
+
console.log(`\n${chalk.green("✓")} Debug profile: ${debugArtifacts.map((f) => chalk.white(f)).join(", ")}`);
|
|
172
|
+
}
|
|
173
|
+
printInitNextSteps(result.options, result.outDir, { installed, debugProfile: debugArtifacts.length > 0 });
|
|
160
174
|
}
|
|
161
175
|
async function handleBoardAdd(options) {
|
|
162
176
|
const { scaffoldBoardPackages, parseBoardSpec, generateFrameworkChecklist } = await import("./create/index.js");
|
|
@@ -442,6 +456,12 @@ async function main() {
|
|
|
442
456
|
let effectiveFrameworkPackage = options.frameworkPackage;
|
|
443
457
|
let effectiveMcuPackage;
|
|
444
458
|
let effectivePort = options.port;
|
|
459
|
+
// CUTTLEFISH_PORT env var sits between the CLI flag and the config file
|
|
460
|
+
// (flag > env > config), so cross-platform uploads don't need a
|
|
461
|
+
// Windows-specific COM port baked into package.json scripts.
|
|
462
|
+
if (!effectivePort && process.env.CUTTLEFISH_PORT) {
|
|
463
|
+
effectivePort = process.env.CUTTLEFISH_PORT;
|
|
464
|
+
}
|
|
445
465
|
if (config) {
|
|
446
466
|
if (config.mcu) {
|
|
447
467
|
effectiveMcuPackage = config.mcu;
|
|
@@ -564,7 +584,6 @@ async function main() {
|
|
|
564
584
|
buildTarget,
|
|
565
585
|
port: effectivePort,
|
|
566
586
|
baud: options.baud ?? config?.console?.baudRate,
|
|
567
|
-
optimize: config?.outputOptimize,
|
|
568
587
|
extraFlags: config?.outputExtraFlags,
|
|
569
588
|
defines: psramDefines,
|
|
570
589
|
psram: config?.psram,
|
|
@@ -665,7 +684,6 @@ async function main() {
|
|
|
665
684
|
buildTarget,
|
|
666
685
|
port: effectivePort,
|
|
667
686
|
baud: options.baud ?? config?.console?.baudRate,
|
|
668
|
-
optimize: config?.outputOptimize,
|
|
669
687
|
extraFlags: config?.outputExtraFlags,
|
|
670
688
|
defines: psramDefines,
|
|
671
689
|
psram: config?.psram,
|
|
@@ -773,7 +791,6 @@ async function main() {
|
|
|
773
791
|
buildTarget,
|
|
774
792
|
port: effectivePort,
|
|
775
793
|
baud: options.baud ?? config?.console?.baudRate,
|
|
776
|
-
optimize: config?.outputOptimize,
|
|
777
794
|
extraFlags: config?.outputExtraFlags,
|
|
778
795
|
defines: psramDefines,
|
|
779
796
|
psram: config?.psram,
|
package/dist/config-loader.d.ts
CHANGED
|
@@ -15,8 +15,6 @@ export interface ResolvedCuttlefishConfig {
|
|
|
15
15
|
buildTarget?: string;
|
|
16
16
|
/** Output framework (e.g. 'arduino'). */
|
|
17
17
|
outputFramework?: string;
|
|
18
|
-
/** Optimization level. */
|
|
19
|
-
outputOptimize?: string;
|
|
20
18
|
/** Output directory. */
|
|
21
19
|
outputOutDir?: string;
|
|
22
20
|
/**
|
|
@@ -52,6 +50,14 @@ export interface ResolvedCuttlefishConfig {
|
|
|
52
50
|
* Returns the absolute path on success, `undefined` if none is found.
|
|
53
51
|
*/
|
|
54
52
|
export declare function findConfigFile(startDir: string): string | undefined;
|
|
53
|
+
/**
|
|
54
|
+
* Warn about config values the AST-only parser cannot evaluate. The loader
|
|
55
|
+
* deliberately never runs user code (no ts-node / dynamic import), so only
|
|
56
|
+
* inline literals survive extraction; without these warnings a value like
|
|
57
|
+
* `libraries: sdlLibraries` (an identifier) used to vanish silently and
|
|
58
|
+
* surface later as an opaque link error.
|
|
59
|
+
*/
|
|
60
|
+
export type ConfigDropWarning = (message: string) => void;
|
|
55
61
|
/**
|
|
56
62
|
*
|
|
57
63
|
* The file must have a default export whose initializer is an object literal.
|
package/dist/config-loader.js
CHANGED
|
@@ -12,6 +12,13 @@ import ts from "typescript";
|
|
|
12
12
|
import { safeValidateConfig } from "./config-schema.js";
|
|
13
13
|
/** The filename we search for when walking up directories. */
|
|
14
14
|
const CONFIG_FILENAME = "cuttlefish.config.ts";
|
|
15
|
+
/** Top-level keys recognized by CuttlefishConfigSchema — used to warn about
|
|
16
|
+
* misspelled keys that the AST extraction would otherwise drop silently. */
|
|
17
|
+
const KNOWN_TOP_LEVEL_KEYS = new Set([
|
|
18
|
+
"entry", "target", "mcu", "board", "contract", "framework", "psram",
|
|
19
|
+
"output", "frameworkData", "include", "exclude", "test", "toolchain",
|
|
20
|
+
"console", "native", "zephyr", "display",
|
|
21
|
+
]);
|
|
15
22
|
/**
|
|
16
23
|
* Search upward from `startDir` for a file named `cuttlefish.config.ts`.
|
|
17
24
|
* Returns the absolute path on success, `undefined` if none is found.
|
|
@@ -30,15 +37,21 @@ export function findConfigFile(startDir) {
|
|
|
30
37
|
}
|
|
31
38
|
return undefined;
|
|
32
39
|
}
|
|
33
|
-
// ---------------------------------------------------------------------------
|
|
34
|
-
// AST helpers — extract scalar values from a TS object literal
|
|
35
|
-
// ---------------------------------------------------------------------------
|
|
36
40
|
function unwrapTypeCast(node) {
|
|
37
41
|
let curr = node;
|
|
38
|
-
|
|
39
|
-
curr
|
|
42
|
+
for (;;) {
|
|
43
|
+
if (ts.isAsExpression(curr) || ts.isTypeAssertionExpression(curr) || ts.isParenthesizedExpression(curr)) {
|
|
44
|
+
curr = curr.expression;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
// satisfies is TS ≥4.9 — guard for older typings, then cast for .expression.
|
|
48
|
+
const isSatisfies = ts.isSatisfiesExpression;
|
|
49
|
+
if (typeof isSatisfies === "function" && isSatisfies(curr)) {
|
|
50
|
+
curr = curr.expression;
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
return curr;
|
|
40
54
|
}
|
|
41
|
-
return curr;
|
|
42
55
|
}
|
|
43
56
|
function getStringLiteral(node) {
|
|
44
57
|
const unwrapped = unwrapTypeCast(node);
|
|
@@ -55,20 +68,51 @@ function getScalarValue(node) {
|
|
|
55
68
|
if (ts.isNumericLiteral(unwrapped)) {
|
|
56
69
|
return Number(unwrapped.text);
|
|
57
70
|
}
|
|
71
|
+
// Negative (and explicitly positive) number literals parse as
|
|
72
|
+
// PrefixUnaryExpression — `reset: -1` used to be silently dropped.
|
|
73
|
+
if (ts.isPrefixUnaryExpression(unwrapped)
|
|
74
|
+
&& (unwrapped.operator === ts.SyntaxKind.MinusToken || unwrapped.operator === ts.SyntaxKind.PlusToken)
|
|
75
|
+
&& ts.isNumericLiteral(unwrapped.operand)) {
|
|
76
|
+
const magnitude = Number(unwrapped.operand.text);
|
|
77
|
+
return unwrapped.operator === ts.SyntaxKind.MinusToken ? -magnitude : magnitude;
|
|
78
|
+
}
|
|
58
79
|
if (unwrapped.kind === ts.SyntaxKind.TrueKeyword)
|
|
59
80
|
return true;
|
|
60
81
|
if (unwrapped.kind === ts.SyntaxKind.FalseKeyword)
|
|
61
82
|
return false;
|
|
62
83
|
return undefined;
|
|
63
84
|
}
|
|
85
|
+
/** True for `null` / `undefined` literals — inline literals, but not values
|
|
86
|
+
* the config shape supports; callers warn with an accurate message instead
|
|
87
|
+
* of the "variables/ternaries" text. */
|
|
88
|
+
function isNullishLiteral(node) {
|
|
89
|
+
const kind = unwrapTypeCast(node).kind;
|
|
90
|
+
return kind === ts.SyntaxKind.NullKeyword || kind === ts.SyntaxKind.UndefinedKeyword;
|
|
91
|
+
}
|
|
64
92
|
/**
|
|
65
93
|
* Walk an object literal and collect all scalar (string / number / boolean)
|
|
66
94
|
* property values into a flat dot-path map — exactly like board-resolver.ts.
|
|
67
95
|
*/
|
|
68
|
-
|
|
96
|
+
/** Property key text for Identifier/StringLiteral names, else undefined
|
|
97
|
+
* (SpreadAssignment has no name; computed keys are not static text). */
|
|
98
|
+
function propertyKeyName(prop) {
|
|
99
|
+
const name = prop.name;
|
|
100
|
+
if (!name)
|
|
101
|
+
return undefined;
|
|
102
|
+
return ts.isIdentifier(name) || ts.isStringLiteral(name) ? name.text : undefined;
|
|
103
|
+
}
|
|
104
|
+
function describePropertyKey(prop) {
|
|
105
|
+
return propertyKeyName(prop) ?? "<unnamed>";
|
|
106
|
+
}
|
|
107
|
+
function walkObjectLiteral(obj, prefix, out, warn) {
|
|
69
108
|
for (const prop of obj.properties) {
|
|
70
|
-
if (!ts.isPropertyAssignment(prop))
|
|
109
|
+
if (!ts.isPropertyAssignment(prop)) {
|
|
110
|
+
if (warn) {
|
|
111
|
+
const label = prefix ? `${prefix}.${describePropertyKey(prop)}` : describePropertyKey(prop);
|
|
112
|
+
warn(`'${label}' uses ${ts.SyntaxKind[prop.kind]} syntax (shorthand/spread/method) — only property assignments are supported, ignored.`);
|
|
113
|
+
}
|
|
71
114
|
continue;
|
|
115
|
+
}
|
|
72
116
|
const key = ts.isIdentifier(prop.name)
|
|
73
117
|
? prop.name.text
|
|
74
118
|
: ts.isStringLiteral(prop.name)
|
|
@@ -78,13 +122,26 @@ function walkObjectLiteral(obj, prefix, out) {
|
|
|
78
122
|
continue;
|
|
79
123
|
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
80
124
|
if (ts.isObjectLiteralExpression(prop.initializer)) {
|
|
81
|
-
walkObjectLiteral(prop.initializer, fullKey, out);
|
|
125
|
+
walkObjectLiteral(prop.initializer, fullKey, out, warn);
|
|
126
|
+
}
|
|
127
|
+
else if (ts.isArrayLiteralExpression(prop.initializer)) {
|
|
128
|
+
// Arrays are extracted by the dedicated array extractors
|
|
129
|
+
// (output.extraFlags, zephyr.cmakeArgs, native.libraries, …), not the
|
|
130
|
+
// flat scalar walk — skip here without warning.
|
|
82
131
|
}
|
|
83
132
|
else {
|
|
84
133
|
const value = getScalarValue(prop.initializer);
|
|
85
134
|
if (value !== undefined) {
|
|
86
135
|
out.set(fullKey, value);
|
|
87
136
|
}
|
|
137
|
+
else if (warn) {
|
|
138
|
+
// Message text must stay byte-identical to extractObjectAsRecord's —
|
|
139
|
+
// sections walked by both (native, display) rely on the warn-site
|
|
140
|
+
// dedup to print one warning per dropped value, not two.
|
|
141
|
+
warn(isNullishLiteral(prop.initializer)
|
|
142
|
+
? `'${fullKey}' is a null/undefined literal — not a supported config value, ignored.`
|
|
143
|
+
: `'${fullKey}' is not an inline literal (variables, ternaries, and template substitutions are not evaluated) — ignored.`);
|
|
144
|
+
}
|
|
88
145
|
}
|
|
89
146
|
}
|
|
90
147
|
}
|
|
@@ -124,20 +181,18 @@ function navigateToObjectProperty(obj, path) {
|
|
|
124
181
|
}
|
|
125
182
|
return undefined;
|
|
126
183
|
}
|
|
127
|
-
function extractStringArray(obj, path) {
|
|
184
|
+
function extractStringArray(obj, path, warn) {
|
|
185
|
+
const label = path.join(".");
|
|
128
186
|
const leaf = navigateToObjectProperty(obj, path);
|
|
129
|
-
if (!leaf || !ts.isArrayLiteralExpression(leaf))
|
|
187
|
+
if (!leaf || !ts.isArrayLiteralExpression(leaf)) {
|
|
188
|
+
if (leaf && warn)
|
|
189
|
+
warn(`'${label}' is not an inline array literal — ignored.`);
|
|
130
190
|
return undefined;
|
|
131
|
-
const result = [];
|
|
132
|
-
for (const elem of leaf.elements) {
|
|
133
|
-
const s = getStringLiteral(elem);
|
|
134
|
-
if (s === undefined)
|
|
135
|
-
return undefined;
|
|
136
|
-
result.push(s);
|
|
137
191
|
}
|
|
138
|
-
return
|
|
192
|
+
return extractStringArrayFromArrayLiteral(leaf, warn, label);
|
|
139
193
|
}
|
|
140
|
-
function extractStringRecord(obj, path) {
|
|
194
|
+
function extractStringRecord(obj, path, warn) {
|
|
195
|
+
const label = path.join(".");
|
|
141
196
|
const leaf = navigateToObjectProperty(obj, path);
|
|
142
197
|
if (!leaf || !ts.isObjectLiteralExpression(leaf))
|
|
143
198
|
return undefined;
|
|
@@ -153,8 +208,11 @@ function extractStringRecord(obj, path) {
|
|
|
153
208
|
if (!key)
|
|
154
209
|
continue;
|
|
155
210
|
const value = getStringLiteral(prop.initializer);
|
|
156
|
-
if (value === undefined)
|
|
211
|
+
if (value === undefined) {
|
|
212
|
+
if (warn)
|
|
213
|
+
warn(`'${label}.${key}' is not a string literal — the whole record is ignored.`);
|
|
157
214
|
return undefined;
|
|
215
|
+
}
|
|
158
216
|
result[key] = value;
|
|
159
217
|
}
|
|
160
218
|
if (Object.keys(result).length === 0)
|
|
@@ -165,11 +223,18 @@ function extractStringRecord(obj, path) {
|
|
|
165
223
|
* Recursively extract an object literal as Record<string, unknown>.
|
|
166
224
|
* Handles strings, numbers, booleans, string arrays, and nested objects.
|
|
167
225
|
*/
|
|
168
|
-
function extractObjectAsRecord(obj) {
|
|
226
|
+
function extractObjectAsRecord(obj, warn, prefix = "") {
|
|
169
227
|
const result = {};
|
|
170
228
|
for (const prop of obj.properties) {
|
|
171
|
-
if (!ts.isPropertyAssignment(prop))
|
|
229
|
+
if (!ts.isPropertyAssignment(prop)) {
|
|
230
|
+
if (warn) {
|
|
231
|
+
// Byte-identical to walkObjectLiteral's message for the same property
|
|
232
|
+
// so the warn-site dedup collapses the double walk into one warning.
|
|
233
|
+
const label = prefix ? `${prefix}.${describePropertyKey(prop)}` : describePropertyKey(prop);
|
|
234
|
+
warn(`'${label}' uses ${ts.SyntaxKind[prop.kind]} syntax (shorthand/spread/method) — only property assignments are supported, ignored.`);
|
|
235
|
+
}
|
|
172
236
|
continue;
|
|
237
|
+
}
|
|
173
238
|
const key = ts.isIdentifier(prop.name)
|
|
174
239
|
? prop.name.text
|
|
175
240
|
: ts.isStringLiteral(prop.name)
|
|
@@ -177,29 +242,40 @@ function extractObjectAsRecord(obj) {
|
|
|
177
242
|
: undefined;
|
|
178
243
|
if (!key)
|
|
179
244
|
continue;
|
|
245
|
+
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
180
246
|
const init = prop.initializer;
|
|
181
247
|
if (ts.isObjectLiteralExpression(init)) {
|
|
182
|
-
result[key] = extractObjectAsRecord(init);
|
|
248
|
+
result[key] = extractObjectAsRecord(init, warn, fullKey);
|
|
183
249
|
}
|
|
184
250
|
else if (ts.isArrayLiteralExpression(init)) {
|
|
185
|
-
const arr = extractStringArrayFromArrayLiteral(init);
|
|
251
|
+
const arr = extractStringArrayFromArrayLiteral(init, warn, fullKey);
|
|
186
252
|
if (arr)
|
|
187
253
|
result[key] = arr;
|
|
188
254
|
}
|
|
189
255
|
else {
|
|
190
256
|
const scalar = getScalarValue(init);
|
|
191
|
-
if (scalar !== undefined)
|
|
257
|
+
if (scalar !== undefined) {
|
|
192
258
|
result[key] = scalar;
|
|
259
|
+
}
|
|
260
|
+
else if (warn) {
|
|
261
|
+
// Keep byte-identical to walkObjectLiteral's message (see above).
|
|
262
|
+
warn(isNullishLiteral(init)
|
|
263
|
+
? `'${fullKey}' is a null/undefined literal — not a supported config value, ignored.`
|
|
264
|
+
: `'${fullKey}' is not an inline literal (variables, ternaries, and template substitutions are not evaluated) — ignored.`);
|
|
265
|
+
}
|
|
193
266
|
}
|
|
194
267
|
}
|
|
195
268
|
return result;
|
|
196
269
|
}
|
|
197
|
-
function extractStringArrayFromArrayLiteral(node) {
|
|
270
|
+
function extractStringArrayFromArrayLiteral(node, warn, label = "array") {
|
|
198
271
|
const result = [];
|
|
199
272
|
for (const elem of node.elements) {
|
|
200
273
|
const s = getStringLiteral(elem);
|
|
201
|
-
if (s === undefined)
|
|
274
|
+
if (s === undefined) {
|
|
275
|
+
if (warn)
|
|
276
|
+
warn(`'${label}' has a non-string-literal element — the whole array is ignored.`);
|
|
202
277
|
return undefined;
|
|
278
|
+
}
|
|
203
279
|
result.push(s);
|
|
204
280
|
}
|
|
205
281
|
return result;
|
|
@@ -207,7 +283,7 @@ function extractStringArrayFromArrayLiteral(node) {
|
|
|
207
283
|
/**
|
|
208
284
|
* Extract a top-level config section as a generic Record<string, unknown>.
|
|
209
285
|
*/
|
|
210
|
-
function extractFrameworkSection(obj, sectionName) {
|
|
286
|
+
function extractFrameworkSection(obj, sectionName, warn) {
|
|
211
287
|
for (const prop of obj.properties) {
|
|
212
288
|
if (!ts.isPropertyAssignment(prop))
|
|
213
289
|
continue;
|
|
@@ -218,9 +294,12 @@ function extractFrameworkSection(obj, sectionName) {
|
|
|
218
294
|
: undefined;
|
|
219
295
|
if (key !== sectionName)
|
|
220
296
|
continue;
|
|
221
|
-
if (!ts.isObjectLiteralExpression(prop.initializer))
|
|
297
|
+
if (!ts.isObjectLiteralExpression(prop.initializer)) {
|
|
298
|
+
if (warn)
|
|
299
|
+
warn(`'${sectionName}' section is not an inline object literal — ignored.`);
|
|
222
300
|
return undefined;
|
|
223
|
-
|
|
301
|
+
}
|
|
302
|
+
return extractObjectAsRecord(prop.initializer, warn, sectionName);
|
|
224
303
|
}
|
|
225
304
|
return undefined;
|
|
226
305
|
}
|
|
@@ -251,11 +330,14 @@ export function parseConfigFile(configPath) {
|
|
|
251
330
|
}
|
|
252
331
|
// export default config; (ExportAssignment with an identifier)
|
|
253
332
|
if (ts.isExportAssignment(stmt) && !stmt.isExportEquals) {
|
|
254
|
-
|
|
255
|
-
|
|
333
|
+
// Unwrap `export default { ... } satisfies CuttlefishConfig` /
|
|
334
|
+
// `as const` / parenthesized forms down to the underlying expression.
|
|
335
|
+
const expr = unwrapTypeCast(stmt.expression);
|
|
336
|
+
if (ts.isIdentifier(expr)) {
|
|
337
|
+
defaultExportName = expr.text;
|
|
256
338
|
}
|
|
257
|
-
else if (ts.isObjectLiteralExpression(
|
|
258
|
-
inlineDefaultObject =
|
|
339
|
+
else if (ts.isObjectLiteralExpression(expr)) {
|
|
340
|
+
inlineDefaultObject = expr;
|
|
259
341
|
}
|
|
260
342
|
}
|
|
261
343
|
}
|
|
@@ -263,16 +345,43 @@ export function parseConfigFile(configPath) {
|
|
|
263
345
|
let configObject = inlineDefaultObject;
|
|
264
346
|
if (!configObject && defaultExportName) {
|
|
265
347
|
const decl = variableDecls.get(defaultExportName);
|
|
266
|
-
if (decl?.initializer
|
|
267
|
-
|
|
348
|
+
if (decl?.initializer) {
|
|
349
|
+
// `const config = { ... } satisfies CuttlefishConfig` parses the
|
|
350
|
+
// initializer as a SatisfiesExpression — unwrap to the object literal
|
|
351
|
+
// so the whole config isn't silently ignored.
|
|
352
|
+
const init = unwrapTypeCast(decl.initializer);
|
|
353
|
+
if (ts.isObjectLiteralExpression(init)) {
|
|
354
|
+
configObject = init;
|
|
355
|
+
}
|
|
268
356
|
}
|
|
269
357
|
}
|
|
270
358
|
if (!configObject) {
|
|
271
359
|
return undefined;
|
|
272
360
|
}
|
|
361
|
+
// Collect drop warnings (values the AST-only parser can't evaluate) and
|
|
362
|
+
// print them once after parsing — silent drops here used to surface much
|
|
363
|
+
// later as missing -l flags / reverted defaults with no diagnostic.
|
|
364
|
+
const dropWarnings = [];
|
|
365
|
+
const seenWarnings = new Set();
|
|
366
|
+
const warn = (message) => {
|
|
367
|
+
const line = `${path.basename(configPath)}: ${message}`;
|
|
368
|
+
if (!seenWarnings.has(line)) {
|
|
369
|
+
seenWarnings.add(line);
|
|
370
|
+
dropWarnings.push(line);
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
// Typos in top-level keys (e.g. `output.optmize`) were dropped before
|
|
374
|
+
// schema validation, so the strict schema never saw them — check the raw
|
|
375
|
+
// source keys against the known set directly.
|
|
376
|
+
for (const prop of configObject.properties) {
|
|
377
|
+
const key = propertyKeyName(prop);
|
|
378
|
+
if (key && !KNOWN_TOP_LEVEL_KEYS.has(key)) {
|
|
379
|
+
warn(`unknown top-level key '${key}' — misspelled or unsupported.`);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
273
382
|
// Walk the object literal into a flat map.
|
|
274
383
|
const flat = new Map();
|
|
275
|
-
walkObjectLiteral(configObject, "", flat);
|
|
384
|
+
walkObjectLiteral(configObject, "", flat, warn);
|
|
276
385
|
// Map flat keys to the resolved config shape.
|
|
277
386
|
const resolved = { configPath };
|
|
278
387
|
const entry = flat.get("entry");
|
|
@@ -298,14 +407,23 @@ export function parseConfigFile(configPath) {
|
|
|
298
407
|
if (typeof framework === "string")
|
|
299
408
|
resolved.framework = framework;
|
|
300
409
|
const psram = flat.get("psram");
|
|
301
|
-
if (
|
|
410
|
+
if (typeof psram === "string") {
|
|
411
|
+
// Assign any string (including "") so the schema's PsramType enum rejects
|
|
412
|
+
// typos ('octal') with a validation error instead of silently dropping it.
|
|
302
413
|
resolved.psram = psram;
|
|
414
|
+
}
|
|
303
415
|
const outputFramework = flat.get("output.framework");
|
|
304
416
|
if (typeof outputFramework === "string")
|
|
305
417
|
resolved.outputFramework = outputFramework;
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
418
|
+
// `output.optimize` is removed: no framework ever consumed it (the only
|
|
419
|
+
// consumer, framework-esp32, is deleted). Warn-and-drop instead of failing
|
|
420
|
+
// strict validation, so existing configs keep building while telling the
|
|
421
|
+
// user to delete the key. Optimization is framework territory — e.g.
|
|
422
|
+
// `zephyr.kconfig` CONFIG_*_OPTIMIZATIONS symbols.
|
|
423
|
+
if (flat.has("output.optimize")) {
|
|
424
|
+
warn("'output.optimize' has no effect and is deprecated — remove it. " +
|
|
425
|
+
"Control optimization via the framework (e.g. zephyr.kconfig CONFIG_SIZE_OPTIMIZATIONS / CONFIG_SPEED_OPTIMIZATIONS).");
|
|
426
|
+
}
|
|
309
427
|
const outputOutDir = flat.get("output.outDir");
|
|
310
428
|
if (typeof outputOutDir === "string")
|
|
311
429
|
resolved.outputOutDir = outputOutDir;
|
|
@@ -319,19 +437,19 @@ export function parseConfigFile(configPath) {
|
|
|
319
437
|
};
|
|
320
438
|
}
|
|
321
439
|
// Extract structured fields that the flat walker cannot handle.
|
|
322
|
-
const outputExtraFlags = extractStringArray(configObject, ["output", "extraFlags"]);
|
|
440
|
+
const outputExtraFlags = extractStringArray(configObject, ["output", "extraFlags"], warn);
|
|
323
441
|
if (outputExtraFlags)
|
|
324
442
|
resolved.outputExtraFlags = outputExtraFlags;
|
|
325
|
-
const outputDefines = extractStringRecord(configObject, ["output", "defines"]);
|
|
443
|
+
const outputDefines = extractStringRecord(configObject, ["output", "defines"], warn);
|
|
326
444
|
if (outputDefines)
|
|
327
445
|
resolved.outputDefines = outputDefines;
|
|
328
|
-
const nativeSection = extractFrameworkSection(configObject, "native");
|
|
446
|
+
const nativeSection = extractFrameworkSection(configObject, "native", warn);
|
|
329
447
|
if (nativeSection) {
|
|
330
448
|
resolved.frameworkConfig = nativeSection;
|
|
331
449
|
}
|
|
332
450
|
// Parse zephyr-specific config section.
|
|
333
|
-
const zephyrKconfig = extractStringRecord(configObject, ["zephyr", "kconfig"]);
|
|
334
|
-
const zephyrCmakeArgs = extractStringArray(configObject, ["zephyr", "cmakeArgs"]);
|
|
451
|
+
const zephyrKconfig = extractStringRecord(configObject, ["zephyr", "kconfig"], warn);
|
|
452
|
+
const zephyrCmakeArgs = extractStringArray(configObject, ["zephyr", "cmakeArgs"], warn);
|
|
335
453
|
const zephyrRunner = flat.get("zephyr.runner");
|
|
336
454
|
if (zephyrKconfig || zephyrCmakeArgs || typeof zephyrRunner === "string") {
|
|
337
455
|
resolved.zephyrConfig = {
|
|
@@ -341,7 +459,10 @@ export function parseConfigFile(configPath) {
|
|
|
341
459
|
};
|
|
342
460
|
}
|
|
343
461
|
// Parse display profile config (nested object with profile name, wiring, touch)
|
|
344
|
-
const displaySection = extractFrameworkSection(configObject, "display");
|
|
462
|
+
const displaySection = extractFrameworkSection(configObject, "display", warn);
|
|
463
|
+
// extractObjectAsRecord walks literals into a loose record; the shape is
|
|
464
|
+
// checked downstream (schema passthrough + display-profile resolution), so
|
|
465
|
+
// bridge it to the declared DisplayConfig type at this single boundary.
|
|
345
466
|
if (displaySection)
|
|
346
467
|
resolved.display = displaySection;
|
|
347
468
|
// Validate the parsed config against the Zod schema.
|
|
@@ -359,12 +480,13 @@ export function parseConfigFile(configPath) {
|
|
|
359
480
|
structuredForValidation.entry = resolved.entry;
|
|
360
481
|
if (resolved.framework)
|
|
361
482
|
structuredForValidation.framework = resolved.framework;
|
|
362
|
-
|
|
483
|
+
// `!== undefined` (not truthiness) so an empty-string psram reaches the
|
|
484
|
+
// PsramType enum and fails validation instead of vanishing.
|
|
485
|
+
if (resolved.psram !== undefined)
|
|
363
486
|
structuredForValidation.psram = resolved.psram;
|
|
364
|
-
if (resolved.outputFramework || resolved.
|
|
487
|
+
if (resolved.outputFramework || resolved.outputOutDir || resolved.outputExtraFlags || resolved.outputDefines) {
|
|
365
488
|
structuredForValidation.output = {
|
|
366
489
|
...(resolved.outputFramework ? { framework: resolved.outputFramework } : {}),
|
|
367
|
-
...(resolved.outputOptimize ? { optimize: resolved.outputOptimize } : {}),
|
|
368
490
|
...(resolved.outputOutDir ? { outDir: resolved.outputOutDir } : {}),
|
|
369
491
|
...(resolved.outputExtraFlags ? { extraFlags: resolved.outputExtraFlags } : {}),
|
|
370
492
|
...(resolved.outputDefines ? { defines: resolved.outputDefines } : {}),
|
|
@@ -374,6 +496,10 @@ export function parseConfigFile(configPath) {
|
|
|
374
496
|
structuredForValidation.console = resolved.console;
|
|
375
497
|
if (resolved.zephyrConfig)
|
|
376
498
|
structuredForValidation.zephyr = resolved.zephyrConfig;
|
|
499
|
+
if (resolved.frameworkConfig)
|
|
500
|
+
structuredForValidation.native = resolved.frameworkConfig;
|
|
501
|
+
if (resolved.display)
|
|
502
|
+
structuredForValidation.display = resolved.display;
|
|
377
503
|
if (resolved.buildTarget) {
|
|
378
504
|
structuredForValidation.frameworkData = { buildTarget: resolved.buildTarget };
|
|
379
505
|
}
|
|
@@ -382,6 +508,12 @@ export function parseConfigFile(configPath) {
|
|
|
382
508
|
const errorMsg = validation.errors.map(e => ` - ${e}`).join("\n");
|
|
383
509
|
throw new Error(`Configuration validation failed for ${configPath}:\n${errorMsg}`);
|
|
384
510
|
}
|
|
511
|
+
if (dropWarnings.length > 0) {
|
|
512
|
+
console.warn(`⚠ ${configPath}: some values were ignored (the parser only evaluates inline literals):`);
|
|
513
|
+
for (const w of dropWarnings) {
|
|
514
|
+
console.warn(` ${w}`);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
385
517
|
return resolved;
|
|
386
518
|
}
|
|
387
519
|
/**
|
|
@@ -440,6 +572,23 @@ export function generateVirtualTypeDeclaration(config, platformDeclarations) {
|
|
|
440
572
|
" type Shared<T = unknown> = T;",
|
|
441
573
|
" type Mutable<T = unknown> = T;",
|
|
442
574
|
"",
|
|
575
|
+
" // SafeVariable: SEU-resistant storage. The transpiler lowers SafeVariable<number>",
|
|
576
|
+
" // to a C++ template with inverted-redundancy storage. Declared as an interface",
|
|
577
|
+
" // (not a type alias) so the TS type checker recognizes method calls.",
|
|
578
|
+
" // Arithmetic T only (integral or floating-point); string is rejected by a",
|
|
579
|
+
" // static_assert in the emitted C++ template.",
|
|
580
|
+
" interface SafeVariable<T = number> { set(value: T): void; get(): T; valid(): boolean; hasFault(): boolean; }",
|
|
581
|
+
" // SafeInt: chainable bounds-checked signed-integer arithmetic. The transpiler",
|
|
582
|
+
" // lowers SafeInt<number> to SafeInt<int32_t> (a C++ template with sticky-fault",
|
|
583
|
+
" // overflow detection). Signed integer T only — unsigned/bool/float/string are",
|
|
584
|
+
" // rejected by a static_assert in the emitted C++ template.",
|
|
585
|
+
" interface SafeInt<T = number> {",
|
|
586
|
+
" add(delta: T): SafeInt<T>; sub(delta: T): SafeInt<T>;",
|
|
587
|
+
" mul(factor: T): SafeInt<T>; divide(d: T): SafeInt<T>; mod(d: T): SafeInt<T>;",
|
|
588
|
+
" negate(): SafeInt<T>; absValue(): SafeInt<T>;",
|
|
589
|
+
" get(): T; hasFault(): boolean; valid(): boolean; reset(newValue: T): void;",
|
|
590
|
+
" }",
|
|
591
|
+
"",
|
|
443
592
|
" // C-style explicit number types recognized by the transpiler",
|
|
444
593
|
" type uint8_t = number;",
|
|
445
594
|
" type int8_t = number;",
|
|
@@ -480,9 +629,9 @@ export function generateVirtualTypeDeclaration(config, platformDeclarations) {
|
|
|
480
629
|
"",
|
|
481
630
|
"declare module '@typecad/board' {",
|
|
482
631
|
` ${boardExport}`,
|
|
483
|
-
" export type Owned<T =
|
|
484
|
-
" export type Shared<T =
|
|
485
|
-
" export type Mutable<T =
|
|
632
|
+
" export type Owned<T = unknown> = T;",
|
|
633
|
+
" export type Shared<T = unknown> = T;",
|
|
634
|
+
" export type Mutable<T = unknown> = T;",
|
|
486
635
|
"}",
|
|
487
636
|
"",
|
|
488
637
|
"export {};",
|
package/dist/config-schema.d.ts
CHANGED
|
@@ -18,19 +18,16 @@ export declare const CuttlefishConfigSchema: z.ZodEffects<z.ZodObject<{
|
|
|
18
18
|
psram: z.ZodOptional<z.ZodEnum<["opi", "quad"]>>;
|
|
19
19
|
output: z.ZodOptional<z.ZodObject<{
|
|
20
20
|
framework: z.ZodOptional<z.ZodString>;
|
|
21
|
-
optimize: z.ZodOptional<z.ZodEnum<["none", "size", "speed", "balanced"]>>;
|
|
22
21
|
outDir: z.ZodOptional<z.ZodString>;
|
|
23
22
|
defines: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
24
23
|
extraFlags: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
25
24
|
}, "strict", z.ZodTypeAny, {
|
|
26
25
|
framework?: string | undefined;
|
|
27
|
-
optimize?: "none" | "size" | "speed" | "balanced" | undefined;
|
|
28
26
|
outDir?: string | undefined;
|
|
29
27
|
defines?: Record<string, string> | undefined;
|
|
30
28
|
extraFlags?: string[] | undefined;
|
|
31
29
|
}, {
|
|
32
30
|
framework?: string | undefined;
|
|
33
|
-
optimize?: "none" | "size" | "speed" | "balanced" | undefined;
|
|
34
31
|
outDir?: string | undefined;
|
|
35
32
|
defines?: Record<string, string> | undefined;
|
|
36
33
|
extraFlags?: string[] | undefined;
|
|
@@ -81,6 +78,9 @@ export declare const CuttlefishConfigSchema: z.ZodEffects<z.ZodObject<{
|
|
|
81
78
|
baudRate?: number | undefined;
|
|
82
79
|
}>>;
|
|
83
80
|
native: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
81
|
+
/** Display profile config (driver, wiring, touch) — loosely typed here so
|
|
82
|
+
* the loader's presence/shape extraction round-trips through validation. */
|
|
83
|
+
display: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
84
84
|
zephyr: z.ZodOptional<z.ZodObject<{
|
|
85
85
|
kconfig: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
86
86
|
cmakeArgs: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
@@ -104,7 +104,6 @@ export declare const CuttlefishConfigSchema: z.ZodEffects<z.ZodObject<{
|
|
|
104
104
|
psram?: "opi" | "quad" | undefined;
|
|
105
105
|
output?: {
|
|
106
106
|
framework?: string | undefined;
|
|
107
|
-
optimize?: "none" | "size" | "speed" | "balanced" | undefined;
|
|
108
107
|
outDir?: string | undefined;
|
|
109
108
|
defines?: Record<string, string> | undefined;
|
|
110
109
|
extraFlags?: string[] | undefined;
|
|
@@ -129,6 +128,7 @@ export declare const CuttlefishConfigSchema: z.ZodEffects<z.ZodObject<{
|
|
|
129
128
|
baudRate?: number | undefined;
|
|
130
129
|
} | undefined;
|
|
131
130
|
native?: Record<string, unknown> | undefined;
|
|
131
|
+
display?: Record<string, unknown> | undefined;
|
|
132
132
|
zephyr?: {
|
|
133
133
|
kconfig?: Record<string, string> | undefined;
|
|
134
134
|
cmakeArgs?: string[] | undefined;
|
|
@@ -144,7 +144,6 @@ export declare const CuttlefishConfigSchema: z.ZodEffects<z.ZodObject<{
|
|
|
144
144
|
psram?: "opi" | "quad" | undefined;
|
|
145
145
|
output?: {
|
|
146
146
|
framework?: string | undefined;
|
|
147
|
-
optimize?: "none" | "size" | "speed" | "balanced" | undefined;
|
|
148
147
|
outDir?: string | undefined;
|
|
149
148
|
defines?: Record<string, string> | undefined;
|
|
150
149
|
extraFlags?: string[] | undefined;
|
|
@@ -169,6 +168,7 @@ export declare const CuttlefishConfigSchema: z.ZodEffects<z.ZodObject<{
|
|
|
169
168
|
baudRate?: number | undefined;
|
|
170
169
|
} | undefined;
|
|
171
170
|
native?: Record<string, unknown> | undefined;
|
|
171
|
+
display?: Record<string, unknown> | undefined;
|
|
172
172
|
zephyr?: {
|
|
173
173
|
kconfig?: Record<string, string> | undefined;
|
|
174
174
|
cmakeArgs?: string[] | undefined;
|
|
@@ -184,7 +184,6 @@ export declare const CuttlefishConfigSchema: z.ZodEffects<z.ZodObject<{
|
|
|
184
184
|
psram?: "opi" | "quad" | undefined;
|
|
185
185
|
output?: {
|
|
186
186
|
framework?: string | undefined;
|
|
187
|
-
optimize?: "none" | "size" | "speed" | "balanced" | undefined;
|
|
188
187
|
outDir?: string | undefined;
|
|
189
188
|
defines?: Record<string, string> | undefined;
|
|
190
189
|
extraFlags?: string[] | undefined;
|
|
@@ -209,6 +208,7 @@ export declare const CuttlefishConfigSchema: z.ZodEffects<z.ZodObject<{
|
|
|
209
208
|
baudRate?: number | undefined;
|
|
210
209
|
} | undefined;
|
|
211
210
|
native?: Record<string, unknown> | undefined;
|
|
211
|
+
display?: Record<string, unknown> | undefined;
|
|
212
212
|
zephyr?: {
|
|
213
213
|
kconfig?: Record<string, string> | undefined;
|
|
214
214
|
cmakeArgs?: string[] | undefined;
|
|
@@ -224,7 +224,6 @@ export declare const CuttlefishConfigSchema: z.ZodEffects<z.ZodObject<{
|
|
|
224
224
|
psram?: "opi" | "quad" | undefined;
|
|
225
225
|
output?: {
|
|
226
226
|
framework?: string | undefined;
|
|
227
|
-
optimize?: "none" | "size" | "speed" | "balanced" | undefined;
|
|
228
227
|
outDir?: string | undefined;
|
|
229
228
|
defines?: Record<string, string> | undefined;
|
|
230
229
|
extraFlags?: string[] | undefined;
|
|
@@ -249,6 +248,7 @@ export declare const CuttlefishConfigSchema: z.ZodEffects<z.ZodObject<{
|
|
|
249
248
|
baudRate?: number | undefined;
|
|
250
249
|
} | undefined;
|
|
251
250
|
native?: Record<string, unknown> | undefined;
|
|
251
|
+
display?: Record<string, unknown> | undefined;
|
|
252
252
|
zephyr?: {
|
|
253
253
|
kconfig?: Record<string, string> | undefined;
|
|
254
254
|
cmakeArgs?: string[] | undefined;
|
package/dist/config-schema.js
CHANGED
|
@@ -6,14 +6,11 @@
|
|
|
6
6
|
// enum constraints before the config is used by the transpiler.
|
|
7
7
|
// ---------------------------------------------------------------------------
|
|
8
8
|
import { z } from 'zod';
|
|
9
|
-
/** Optimization levels accepted by the output.optimize field. */
|
|
10
|
-
const OptimizationLevel = z.enum(['none', 'size', 'speed', 'balanced']);
|
|
11
9
|
/** PSRAM types accepted by the top-level `psram` field (ESP32 PSRAM variants). */
|
|
12
10
|
const PsramType = z.enum(['opi', 'quad']);
|
|
13
11
|
/** Schema for the `output` section. */
|
|
14
12
|
const OutputConfig = z.object({
|
|
15
13
|
framework: z.string().min(1).optional(),
|
|
16
|
-
optimize: OptimizationLevel.optional(),
|
|
17
14
|
outDir: z.string().optional(),
|
|
18
15
|
defines: z.record(z.string(), z.string()).optional(),
|
|
19
16
|
extraFlags: z.array(z.string()).optional(),
|
|
@@ -68,6 +65,9 @@ export const CuttlefishConfigSchema = z.object({
|
|
|
68
65
|
toolchain: ToolchainConfig.optional(),
|
|
69
66
|
console: ConsoleConfig.optional(),
|
|
70
67
|
native: z.record(z.string(), z.unknown()).optional(),
|
|
68
|
+
/** Display profile config (driver, wiring, touch) — loosely typed here so
|
|
69
|
+
* the loader's presence/shape extraction round-trips through validation. */
|
|
70
|
+
display: z.record(z.string(), z.unknown()).optional(),
|
|
71
71
|
zephyr: ZephyrConfig.optional(),
|
|
72
72
|
}).strict().refine(data => !(data.board && data.contract), {
|
|
73
73
|
message: "Specifying both 'board' and 'contract' is not allowed. Choose one.",
|