imxc 0.5.3 → 0.6.0
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/compile.d.ts +8 -0
- package/dist/compile.js +290 -26
- package/dist/components.js +597 -45
- package/dist/diagnostics.js +3 -2
- package/dist/emitter.d.ts +6 -3
- package/dist/emitter.js +1454 -322
- package/dist/index.js +34 -7
- package/dist/init.d.ts +7 -1
- package/dist/init.js +43 -455
- package/dist/ir.d.ts +437 -5
- package/dist/lowering.d.ts +4 -3
- package/dist/lowering.js +770 -57
- package/dist/parser.d.ts +1 -0
- package/dist/templates/async.d.ts +1 -0
- package/dist/templates/async.js +228 -0
- package/dist/templates/custom.d.ts +15 -0
- package/dist/templates/custom.js +945 -0
- package/dist/templates/filedialog.d.ts +1 -0
- package/dist/templates/filedialog.js +216 -0
- package/dist/templates/hotreload.d.ts +1 -0
- package/dist/templates/hotreload.js +400 -0
- package/dist/templates/index.d.ts +16 -0
- package/dist/templates/index.js +553 -0
- package/dist/templates/minimal.d.ts +1 -0
- package/dist/templates/minimal.js +165 -0
- package/dist/templates/networking.d.ts +1 -0
- package/dist/templates/networking.js +244 -0
- package/dist/templates/persistence.d.ts +1 -0
- package/dist/templates/persistence.js +238 -0
- package/dist/validator.d.ts +1 -0
- package/dist/validator.js +51 -22
- package/dist/watch.d.ts +2 -1
- package/dist/watch.js +21 -4
- package/package.json +2 -4
package/dist/compile.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export interface CompileResult {
|
|
|
3
3
|
success: boolean;
|
|
4
4
|
componentCount: number;
|
|
5
5
|
errors: string[];
|
|
6
|
+
warnings: string[];
|
|
6
7
|
}
|
|
7
8
|
/**
|
|
8
9
|
* Compile a list of .tsx files and write generated C++ to outputDir.
|
|
@@ -10,3 +11,10 @@ export interface CompileResult {
|
|
|
10
11
|
*/
|
|
11
12
|
export declare function compile(files: string[], outputDir: string): CompileResult;
|
|
12
13
|
export declare function resolveDragDropTypes(nodes: IRNode[]): void;
|
|
14
|
+
export interface FontDeclaration {
|
|
15
|
+
name: string;
|
|
16
|
+
src: string;
|
|
17
|
+
size: string;
|
|
18
|
+
embed: boolean;
|
|
19
|
+
embedKey?: string;
|
|
20
|
+
}
|
package/dist/compile.js
CHANGED
|
@@ -14,7 +14,9 @@ export function compile(files, outputDir) {
|
|
|
14
14
|
fs.mkdirSync(outputDir, { recursive: true });
|
|
15
15
|
let hasErrors = false;
|
|
16
16
|
const errorMessages = [];
|
|
17
|
+
const warningMessages = [];
|
|
17
18
|
const compiled = [];
|
|
19
|
+
const allExternalInterfaces = new Map();
|
|
18
20
|
// Phase 1: Parse, validate, and lower all components
|
|
19
21
|
for (const file of files) {
|
|
20
22
|
if (!fs.existsSync(file)) {
|
|
@@ -31,6 +33,9 @@ export function compile(files, outputDir) {
|
|
|
31
33
|
continue;
|
|
32
34
|
}
|
|
33
35
|
const validation = validate(parsed);
|
|
36
|
+
if (validation.warnings.length > 0) {
|
|
37
|
+
validation.warnings.forEach(w => warningMessages.push(formatDiagnostic(w, source)));
|
|
38
|
+
}
|
|
34
39
|
if (validation.errors.length > 0) {
|
|
35
40
|
validation.errors.forEach(e => errorMessages.push(formatDiagnostic(e, source)));
|
|
36
41
|
hasErrors = true;
|
|
@@ -38,6 +43,8 @@ export function compile(files, outputDir) {
|
|
|
38
43
|
}
|
|
39
44
|
// Load external interface definitions from imx.d.ts in the same directory
|
|
40
45
|
const externalInterfaces = loadExternalInterfaces(path.dirname(path.resolve(file)));
|
|
46
|
+
for (const [k, v] of externalInterfaces)
|
|
47
|
+
allExternalInterfaces.set(k, v);
|
|
41
48
|
const ir = lowerComponent(parsed, validation, externalInterfaces);
|
|
42
49
|
const imports = extractImports(parsed.sourceFile);
|
|
43
50
|
compiled.push({
|
|
@@ -53,7 +60,7 @@ export function compile(files, outputDir) {
|
|
|
53
60
|
});
|
|
54
61
|
}
|
|
55
62
|
if (hasErrors) {
|
|
56
|
-
return { success: false, componentCount: 0, errors: errorMessages };
|
|
63
|
+
return { success: false, componentCount: 0, errors: errorMessages, warnings: warningMessages };
|
|
57
64
|
}
|
|
58
65
|
// Phase 2: Build lookup of compiled components for cross-file resolution
|
|
59
66
|
const componentMap = new Map();
|
|
@@ -64,25 +71,64 @@ export function compile(files, outputDir) {
|
|
|
64
71
|
for (const comp of compiled) {
|
|
65
72
|
resolveCustomComponents(comp.ir.body, componentMap);
|
|
66
73
|
resolveDragDropTypes(comp.ir.body);
|
|
67
|
-
|
|
74
|
+
// Only detect bound props for custom components (inline props).
|
|
75
|
+
// Root components with namedPropsType receive T& directly — no pointer wrapping needed.
|
|
76
|
+
if (!comp.ir.namedPropsType) {
|
|
77
|
+
comp.boundProps = detectBoundProps(comp.ir.body);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// Propagate bound props through component call chains:
|
|
81
|
+
// If component X passes props.foo to a bound prop of child component Y,
|
|
82
|
+
// then foo is also bound in X (needs to be a pointer too).
|
|
83
|
+
let changed = true;
|
|
84
|
+
while (changed) {
|
|
85
|
+
changed = false;
|
|
86
|
+
for (const comp of compiled) {
|
|
87
|
+
if (comp.ir.namedPropsType)
|
|
88
|
+
continue;
|
|
89
|
+
const before = comp.boundProps.size;
|
|
90
|
+
propagateBoundProps(comp.ir.body, comp.boundProps, componentMap);
|
|
91
|
+
if (comp.boundProps.size > before)
|
|
92
|
+
changed = true;
|
|
93
|
+
}
|
|
68
94
|
}
|
|
69
95
|
// Build boundProps map for cross-component emitter use
|
|
70
96
|
const boundPropsMap = new Map();
|
|
71
97
|
for (const comp of compiled) {
|
|
72
98
|
boundPropsMap.set(comp.name, comp.boundProps);
|
|
73
99
|
}
|
|
100
|
+
const sharedPropsType = compiled.find(c => c.ir.namedPropsType)?.ir.namedPropsType;
|
|
101
|
+
// Resolve actual C++ types for bound props by tracing through parent interfaces.
|
|
102
|
+
// When a child declares `speed: number` (→ 'int'), but the parent struct has `float speed`,
|
|
103
|
+
// the bound prop pointer must use the parent's actual type.
|
|
104
|
+
const resolvedBoundPropTypes = new Map();
|
|
105
|
+
for (const comp of compiled) {
|
|
106
|
+
// Build field types for this component (either from external interface or inline params)
|
|
107
|
+
let fieldTypes;
|
|
108
|
+
if (comp.ir.namedPropsType) {
|
|
109
|
+
fieldTypes = allExternalInterfaces.get(comp.ir.namedPropsType);
|
|
110
|
+
}
|
|
111
|
+
else if (comp.ir.params.length > 0) {
|
|
112
|
+
fieldTypes = new Map();
|
|
113
|
+
for (const p of comp.ir.params)
|
|
114
|
+
fieldTypes.set(p.name, p.type);
|
|
115
|
+
}
|
|
116
|
+
if (fieldTypes) {
|
|
117
|
+
resolveChildBoundTypes(comp.ir.body, fieldTypes, componentMap, allExternalInterfaces, resolvedBoundPropTypes);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
74
120
|
for (const comp of compiled) {
|
|
75
121
|
const importInfos = [];
|
|
76
122
|
for (const [importedName] of comp.imports) {
|
|
77
123
|
const importedComp = componentMap.get(importedName);
|
|
78
|
-
if (importedComp
|
|
124
|
+
if (importedComp) {
|
|
79
125
|
importInfos.push({
|
|
80
126
|
name: importedName,
|
|
81
127
|
headerFile: `${importedName}.gen.h`,
|
|
82
128
|
});
|
|
83
129
|
}
|
|
84
130
|
}
|
|
85
|
-
const cppOutput = emitComponent(comp.ir, importInfos, comp.sourceFile, comp.boundProps, boundPropsMap);
|
|
131
|
+
const cppOutput = emitComponent(comp.ir, importInfos, comp.sourceFile, comp.boundProps, boundPropsMap, { sourceMap: true });
|
|
86
132
|
const baseName = comp.name;
|
|
87
133
|
const outPath = path.join(outputDir, `${baseName}.gen.cpp`);
|
|
88
134
|
fs.writeFileSync(outPath, cppOutput);
|
|
@@ -93,15 +139,28 @@ export function compile(files, outputDir) {
|
|
|
93
139
|
const sourceDir = path.dirname(path.resolve(comp.sourcePath));
|
|
94
140
|
generateEmbedHeaders(embedImages, sourceDir, outputDir);
|
|
95
141
|
}
|
|
96
|
-
//
|
|
97
|
-
// those are declared in the user's main.cpp)
|
|
98
|
-
|
|
99
|
-
|
|
142
|
+
// Generate a header for non-root components (not for named interface types —
|
|
143
|
+
// those are declared in the user's main.cpp). Even propless components need
|
|
144
|
+
// a forward declaration so the root can call their render function.
|
|
145
|
+
if (!comp.ir.namedPropsType && (comp.hasProps || comp !== compiled[0])) {
|
|
146
|
+
const resolved = resolvedBoundPropTypes.get(comp.name);
|
|
147
|
+
const headerOutput = emitComponentHeader(comp.ir, comp.sourceFile, comp.boundProps, sharedPropsType, resolved);
|
|
100
148
|
const headerPath = path.join(outputDir, `${baseName}.gen.h`);
|
|
101
149
|
fs.writeFileSync(headerPath, headerOutput);
|
|
102
150
|
console.log(` ${baseName} -> ${headerPath} (header)`);
|
|
103
151
|
}
|
|
104
152
|
}
|
|
153
|
+
// Collect font declarations from ALL components
|
|
154
|
+
const allFontDeclarations = [];
|
|
155
|
+
for (const comp of compiled) {
|
|
156
|
+
allFontDeclarations.push(...collectFontDeclarations(comp.ir.body));
|
|
157
|
+
}
|
|
158
|
+
const fontDeclarations = deduplicateFonts(allFontDeclarations);
|
|
159
|
+
// Generate embed headers for fonts
|
|
160
|
+
if (fontDeclarations.some(f => f.embed)) {
|
|
161
|
+
const sourceDir = path.dirname(path.resolve(compiled[0].sourcePath));
|
|
162
|
+
generateFontEmbedHeaders(fontDeclarations, sourceDir, outputDir);
|
|
163
|
+
}
|
|
105
164
|
// Phase 4: Emit root entry point
|
|
106
165
|
if (compiled.length > 0) {
|
|
107
166
|
const root = compiled[0];
|
|
@@ -110,12 +169,12 @@ export function compile(files, outputDir) {
|
|
|
110
169
|
const propsType = root.ir.namedPropsType
|
|
111
170
|
? root.ir.namedPropsType
|
|
112
171
|
: root.hasProps ? root.name + 'Props' : undefined;
|
|
113
|
-
const rootOutput = emitRoot(root.name, root.stateCount, root.bufferCount, root.sourceFile, propsType, isNamedPropsType);
|
|
172
|
+
const rootOutput = emitRoot(root.name, root.stateCount, root.bufferCount, root.sourceFile, propsType, isNamedPropsType, fontDeclarations);
|
|
114
173
|
const rootPath = path.join(outputDir, 'app_root.gen.cpp');
|
|
115
174
|
fs.writeFileSync(rootPath, rootOutput);
|
|
116
175
|
console.log(` -> ${rootPath} (root entry point)`);
|
|
117
176
|
}
|
|
118
|
-
return { success: true, componentCount: compiled.length, errors: [] };
|
|
177
|
+
return { success: true, componentCount: compiled.length, errors: [], warnings: warningMessages };
|
|
119
178
|
}
|
|
120
179
|
function resolveCustomComponents(nodes, map) {
|
|
121
180
|
for (const node of nodes) {
|
|
@@ -206,8 +265,15 @@ function generateEmbedHeaders(images, sourceDir, outputDir) {
|
|
|
206
265
|
if (!img.embedKey)
|
|
207
266
|
continue;
|
|
208
267
|
const rawSrc = img.src.replace(/^"|"$/g, '');
|
|
209
|
-
|
|
268
|
+
let imagePath = path.resolve(sourceDir, rawSrc);
|
|
210
269
|
const headerPath = path.join(outputDir, `${img.embedKey}.embed.h`);
|
|
270
|
+
// Try public/ subdirectory as fallback (relative to sourceDir's parent)
|
|
271
|
+
if (!fs.existsSync(imagePath)) {
|
|
272
|
+
const publicPath = path.resolve(sourceDir, '..', 'public', rawSrc);
|
|
273
|
+
if (fs.existsSync(publicPath)) {
|
|
274
|
+
imagePath = publicPath;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
211
277
|
// Mtime caching: skip if header exists and is newer than image
|
|
212
278
|
if (fs.existsSync(headerPath) && fs.existsSync(imagePath)) {
|
|
213
279
|
const imgStat = fs.statSync(imagePath);
|
|
@@ -217,7 +283,7 @@ function generateEmbedHeaders(images, sourceDir, outputDir) {
|
|
|
217
283
|
}
|
|
218
284
|
}
|
|
219
285
|
if (!fs.existsSync(imagePath)) {
|
|
220
|
-
console.warn(` warning: embedded image not found: ${imagePath}`);
|
|
286
|
+
console.warn(` warning: embedded image not found: ${imagePath} (also tried public/)`);
|
|
221
287
|
continue;
|
|
222
288
|
}
|
|
223
289
|
const imageData = fs.readFileSync(imagePath);
|
|
@@ -235,11 +301,113 @@ function generateEmbedHeaders(images, sourceDir, outputDir) {
|
|
|
235
301
|
console.log(` ${rawSrc} -> ${headerPath} (embed)`);
|
|
236
302
|
}
|
|
237
303
|
}
|
|
304
|
+
function collectFontDeclarations(nodes) {
|
|
305
|
+
const fonts = [];
|
|
306
|
+
for (const node of nodes) {
|
|
307
|
+
if (node.kind === 'begin_container' && node.tag === 'Font' && node.props['src']) {
|
|
308
|
+
const name = (node.props['name'] ?? '').replace(/^"|"$/g, '');
|
|
309
|
+
const src = (node.props['src'] ?? '').replace(/^"|"$/g, '');
|
|
310
|
+
const size = node.props['size'] ?? '16.0f';
|
|
311
|
+
const embed = node.props['embed'] === 'true';
|
|
312
|
+
let embedKey;
|
|
313
|
+
if (embed) {
|
|
314
|
+
embedKey = src.replace(/[^a-zA-Z0-9]/g, '_');
|
|
315
|
+
}
|
|
316
|
+
fonts.push({ name, src, size, embed, embedKey });
|
|
317
|
+
}
|
|
318
|
+
else if (node.kind === 'conditional') {
|
|
319
|
+
fonts.push(...collectFontDeclarations(node.body));
|
|
320
|
+
if (node.elseBody)
|
|
321
|
+
fonts.push(...collectFontDeclarations(node.elseBody));
|
|
322
|
+
}
|
|
323
|
+
else if (node.kind === 'list_map') {
|
|
324
|
+
fonts.push(...collectFontDeclarations(node.body));
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return fonts;
|
|
328
|
+
}
|
|
329
|
+
function deduplicateFonts(fonts) {
|
|
330
|
+
const seen = new Map();
|
|
331
|
+
for (const f of fonts) {
|
|
332
|
+
if (seen.has(f.name)) {
|
|
333
|
+
const existing = seen.get(f.name);
|
|
334
|
+
if (existing.src !== f.src) {
|
|
335
|
+
console.error(` error: font "${f.name}" declared with different sources: "${existing.src}" vs "${f.src}"`);
|
|
336
|
+
}
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
seen.set(f.name, f);
|
|
340
|
+
}
|
|
341
|
+
return Array.from(seen.values());
|
|
342
|
+
}
|
|
343
|
+
function generateFontEmbedHeaders(fonts, sourceDir, outputDir) {
|
|
344
|
+
for (const font of fonts) {
|
|
345
|
+
if (!font.embed || !font.embedKey)
|
|
346
|
+
continue;
|
|
347
|
+
let fontPath = path.resolve(sourceDir, font.src);
|
|
348
|
+
const headerPath = path.join(outputDir, `${font.embedKey}.embed.h`);
|
|
349
|
+
// Mtime caching: skip if header exists and is newer than font file
|
|
350
|
+
if (fs.existsSync(headerPath) && fs.existsSync(fontPath)) {
|
|
351
|
+
const fontStat = fs.statSync(fontPath);
|
|
352
|
+
const hdrStat = fs.statSync(headerPath);
|
|
353
|
+
if (hdrStat.mtimeMs >= fontStat.mtimeMs) {
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
if (!fs.existsSync(fontPath)) {
|
|
358
|
+
// Try public/ subdirectory (relative to sourceDir's parent, for src/ layout)
|
|
359
|
+
const publicPath = path.resolve(sourceDir, '..', 'public', font.src);
|
|
360
|
+
if (fs.existsSync(publicPath)) {
|
|
361
|
+
fontPath = publicPath;
|
|
362
|
+
}
|
|
363
|
+
else {
|
|
364
|
+
console.warn(` warning: embedded font not found: ${fontPath} (also tried public/)`);
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
const fontData = fs.readFileSync(fontPath);
|
|
369
|
+
const bytes = Array.from(fontData)
|
|
370
|
+
.map(b => `0x${b.toString(16).padStart(2, '0')}`)
|
|
371
|
+
.join(', ');
|
|
372
|
+
const header = [
|
|
373
|
+
`// Generated from ${font.src} by imxc`,
|
|
374
|
+
`#pragma once`,
|
|
375
|
+
`static const unsigned char ${font.embedKey}_data[] = { ${bytes} };`,
|
|
376
|
+
`static const unsigned int ${font.embedKey}_size = ${fontData.length};`,
|
|
377
|
+
'',
|
|
378
|
+
].join('\n');
|
|
379
|
+
fs.writeFileSync(headerPath, header);
|
|
380
|
+
console.log(` ${font.src} -> ${headerPath} (font embed)`);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
238
383
|
function detectBoundProps(nodes) {
|
|
239
384
|
const bound = new Set();
|
|
240
385
|
walkNodesForBinding(nodes, bound);
|
|
241
386
|
return bound;
|
|
242
387
|
}
|
|
388
|
+
function propagateBoundProps(nodes, bound, componentMap) {
|
|
389
|
+
for (const node of nodes) {
|
|
390
|
+
if (node.kind === 'custom_component') {
|
|
391
|
+
const child = componentMap.get(node.name);
|
|
392
|
+
if (child) {
|
|
393
|
+
for (const [propName, valueExpr] of Object.entries(node.props)) {
|
|
394
|
+
if (child.boundProps.has(propName) && valueExpr.startsWith('props.')) {
|
|
395
|
+
const parentProp = valueExpr.slice(6).split('.')[0].split('[')[0];
|
|
396
|
+
bound.add(parentProp);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
else if (node.kind === 'conditional') {
|
|
402
|
+
propagateBoundProps(node.body, bound, componentMap);
|
|
403
|
+
if (node.elseBody)
|
|
404
|
+
propagateBoundProps(node.elseBody, bound, componentMap);
|
|
405
|
+
}
|
|
406
|
+
else if (node.kind === 'list_map') {
|
|
407
|
+
propagateBoundProps(node.body, bound, componentMap);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
}
|
|
243
411
|
function walkNodesForBinding(nodes, bound) {
|
|
244
412
|
for (const node of nodes) {
|
|
245
413
|
if ('directBind' in node && node.directBind && 'valueExpr' in node) {
|
|
@@ -249,6 +417,10 @@ function walkNodesForBinding(nodes, bound) {
|
|
|
249
417
|
bound.add(propName);
|
|
250
418
|
}
|
|
251
419
|
}
|
|
420
|
+
// Scan all expressions for nested struct field access (props.X.Y or props.X[N]).
|
|
421
|
+
// This catches struct binding in callbacks, conditions, selections, text args, etc.
|
|
422
|
+
// — not just directBind widgets.
|
|
423
|
+
scanNodeExprsForBinding(node, bound);
|
|
252
424
|
if (node.kind === 'conditional') {
|
|
253
425
|
walkNodesForBinding(node.body, bound);
|
|
254
426
|
if (node.elseBody)
|
|
@@ -259,10 +431,115 @@ function walkNodesForBinding(nodes, bound) {
|
|
|
259
431
|
}
|
|
260
432
|
}
|
|
261
433
|
}
|
|
434
|
+
/** Scan all string-valued properties of an IR node for props.X.Y / props.X[N] patterns. */
|
|
435
|
+
function scanNodeExprsForBinding(node, bound) {
|
|
436
|
+
const obj = node;
|
|
437
|
+
for (const key of Object.keys(obj)) {
|
|
438
|
+
// Skip structural/recursive fields — they're handled by walkNodes recursion
|
|
439
|
+
if (key === 'kind' || key === 'body' || key === 'elseBody' || key === 'loc')
|
|
440
|
+
continue;
|
|
441
|
+
const val = obj[key];
|
|
442
|
+
if (typeof val === 'string') {
|
|
443
|
+
extractNestedPropAccess(val, bound);
|
|
444
|
+
}
|
|
445
|
+
else if (Array.isArray(val)) {
|
|
446
|
+
for (const item of val) {
|
|
447
|
+
if (typeof item === 'string')
|
|
448
|
+
extractNestedPropAccess(item, bound);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
else if (typeof val === 'object' && val !== null) {
|
|
452
|
+
// Handles Record<string, string> (container props, custom component props)
|
|
453
|
+
// and IRItemInteraction objects
|
|
454
|
+
for (const v of Object.values(val)) {
|
|
455
|
+
if (typeof v === 'string')
|
|
456
|
+
extractNestedPropAccess(v, bound);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
/** If expr contains props.X.Y or props.X[N], mark X as needing pointer binding.
|
|
462
|
+
* Skips .c_str() and .size() — these are method calls on scalar types, not struct access. */
|
|
463
|
+
function extractNestedPropAccess(expr, bound) {
|
|
464
|
+
// Match props.X. (dot access)
|
|
465
|
+
const dotRegex = /props\.(\w+)\./g;
|
|
466
|
+
let match;
|
|
467
|
+
while ((match = dotRegex.exec(expr)) !== null) {
|
|
468
|
+
const afterDot = expr.slice(match.index + match[0].length);
|
|
469
|
+
if (afterDot.startsWith('c_str()') || afterDot.startsWith('size()'))
|
|
470
|
+
continue;
|
|
471
|
+
bound.add(match[1]);
|
|
472
|
+
}
|
|
473
|
+
// Match props.X[ (bracket access)
|
|
474
|
+
const bracketRegex = /props\.(\w+)\[/g;
|
|
475
|
+
while ((match = bracketRegex.exec(expr)) !== null) {
|
|
476
|
+
bound.add(match[1]);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Walk IR nodes in a parent component, resolving actual C++ types for child bound props.
|
|
481
|
+
* When a child has `speed: number` (→ 'int') but the parent passes props.speed from a struct
|
|
482
|
+
* where speed is float, the resolved type overrides the child's inferred type.
|
|
483
|
+
*/
|
|
484
|
+
function resolveChildBoundTypes(nodes, parentFieldTypes, componentMap, extIfaces, result) {
|
|
485
|
+
for (const node of nodes) {
|
|
486
|
+
if (node.kind === 'custom_component') {
|
|
487
|
+
const child = componentMap.get(node.name);
|
|
488
|
+
if (child) {
|
|
489
|
+
for (const [propName, valueExpr] of Object.entries(node.props)) {
|
|
490
|
+
if (!child.boundProps.has(propName) || !valueExpr.startsWith('props.'))
|
|
491
|
+
continue;
|
|
492
|
+
const parts = valueExpr.slice(6).split('.');
|
|
493
|
+
const topField = parts[0].split('[')[0];
|
|
494
|
+
const topType = parentFieldTypes.get(topField);
|
|
495
|
+
if (!topType || topType === 'callback')
|
|
496
|
+
continue;
|
|
497
|
+
let resolvedType;
|
|
498
|
+
if (parts.length === 1) {
|
|
499
|
+
// Direct scalar: props.speed → parent's type for speed
|
|
500
|
+
resolvedType = topType;
|
|
501
|
+
}
|
|
502
|
+
else if (parts.length === 2) {
|
|
503
|
+
// Nested: props.data.speed → look up speed in data's interface
|
|
504
|
+
const iface = extIfaces.get(topType);
|
|
505
|
+
if (iface) {
|
|
506
|
+
const ft = iface.get(parts[1].split('[')[0]);
|
|
507
|
+
if (ft && ft !== 'callback')
|
|
508
|
+
resolvedType = ft;
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
if (resolvedType) {
|
|
512
|
+
if (!result.has(node.name))
|
|
513
|
+
result.set(node.name, new Map());
|
|
514
|
+
result.get(node.name).set(propName, resolvedType);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
else if (node.kind === 'conditional') {
|
|
520
|
+
resolveChildBoundTypes(node.body, parentFieldTypes, componentMap, extIfaces, result);
|
|
521
|
+
if (node.elseBody)
|
|
522
|
+
resolveChildBoundTypes(node.elseBody, parentFieldTypes, componentMap, extIfaces, result);
|
|
523
|
+
}
|
|
524
|
+
else if (node.kind === 'list_map') {
|
|
525
|
+
resolveChildBoundTypes(node.body, parentFieldTypes, componentMap, extIfaces, result);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
262
529
|
/**
|
|
263
530
|
* Parse the imx.d.ts in the given directory (if present) and extract
|
|
264
531
|
* all interface declarations as a map from interface name -> field name -> type.
|
|
265
532
|
*/
|
|
533
|
+
function normalizeExternalPropType(typeText) {
|
|
534
|
+
const trimmed = typeText.trim().replace(/\s*\|\s*undefined$/, '');
|
|
535
|
+
if (trimmed === 'number')
|
|
536
|
+
return 'float';
|
|
537
|
+
if (trimmed === 'boolean')
|
|
538
|
+
return 'bool';
|
|
539
|
+
if (trimmed === 'string')
|
|
540
|
+
return 'string';
|
|
541
|
+
return trimmed;
|
|
542
|
+
}
|
|
266
543
|
function loadExternalInterfaces(dir) {
|
|
267
544
|
const result = new Map();
|
|
268
545
|
const dtsPath = path.join(dir, 'imx.d.ts');
|
|
@@ -285,20 +562,7 @@ function loadExternalInterfaces(dir) {
|
|
|
285
562
|
fields.set(fieldName, 'callback');
|
|
286
563
|
continue;
|
|
287
564
|
}
|
|
288
|
-
|
|
289
|
-
if (typeText === 'number') {
|
|
290
|
-
fields.set(fieldName, 'float');
|
|
291
|
-
}
|
|
292
|
-
else if (typeText === 'boolean') {
|
|
293
|
-
fields.set(fieldName, 'bool');
|
|
294
|
-
}
|
|
295
|
-
else if (typeText === 'string') {
|
|
296
|
-
fields.set(fieldName, 'string');
|
|
297
|
-
}
|
|
298
|
-
else {
|
|
299
|
-
// Arrays, nested interfaces, etc. treated as opaque (non-scalar)
|
|
300
|
-
fields.set(fieldName, 'string');
|
|
301
|
-
}
|
|
565
|
+
fields.set(fieldName, normalizeExternalPropType(member.type.getText(sf)));
|
|
302
566
|
}
|
|
303
567
|
else if (ts.isMethodSignature(member)) {
|
|
304
568
|
const mName = ts.isIdentifier(member.name) ? member.name.text : '';
|