imxc 0.5.4 → 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 +285 -25
- 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();
|
|
@@ -70,23 +77,58 @@ export function compile(files, outputDir) {
|
|
|
70
77
|
comp.boundProps = detectBoundProps(comp.ir.body);
|
|
71
78
|
}
|
|
72
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
|
+
}
|
|
94
|
+
}
|
|
73
95
|
// Build boundProps map for cross-component emitter use
|
|
74
96
|
const boundPropsMap = new Map();
|
|
75
97
|
for (const comp of compiled) {
|
|
76
98
|
boundPropsMap.set(comp.name, comp.boundProps);
|
|
77
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
|
+
}
|
|
78
120
|
for (const comp of compiled) {
|
|
79
121
|
const importInfos = [];
|
|
80
122
|
for (const [importedName] of comp.imports) {
|
|
81
123
|
const importedComp = componentMap.get(importedName);
|
|
82
|
-
if (importedComp
|
|
124
|
+
if (importedComp) {
|
|
83
125
|
importInfos.push({
|
|
84
126
|
name: importedName,
|
|
85
127
|
headerFile: `${importedName}.gen.h`,
|
|
86
128
|
});
|
|
87
129
|
}
|
|
88
130
|
}
|
|
89
|
-
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 });
|
|
90
132
|
const baseName = comp.name;
|
|
91
133
|
const outPath = path.join(outputDir, `${baseName}.gen.cpp`);
|
|
92
134
|
fs.writeFileSync(outPath, cppOutput);
|
|
@@ -97,15 +139,28 @@ export function compile(files, outputDir) {
|
|
|
97
139
|
const sourceDir = path.dirname(path.resolve(comp.sourcePath));
|
|
98
140
|
generateEmbedHeaders(embedImages, sourceDir, outputDir);
|
|
99
141
|
}
|
|
100
|
-
//
|
|
101
|
-
// those are declared in the user's main.cpp)
|
|
102
|
-
|
|
103
|
-
|
|
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);
|
|
104
148
|
const headerPath = path.join(outputDir, `${baseName}.gen.h`);
|
|
105
149
|
fs.writeFileSync(headerPath, headerOutput);
|
|
106
150
|
console.log(` ${baseName} -> ${headerPath} (header)`);
|
|
107
151
|
}
|
|
108
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
|
+
}
|
|
109
164
|
// Phase 4: Emit root entry point
|
|
110
165
|
if (compiled.length > 0) {
|
|
111
166
|
const root = compiled[0];
|
|
@@ -114,12 +169,12 @@ export function compile(files, outputDir) {
|
|
|
114
169
|
const propsType = root.ir.namedPropsType
|
|
115
170
|
? root.ir.namedPropsType
|
|
116
171
|
: root.hasProps ? root.name + 'Props' : undefined;
|
|
117
|
-
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);
|
|
118
173
|
const rootPath = path.join(outputDir, 'app_root.gen.cpp');
|
|
119
174
|
fs.writeFileSync(rootPath, rootOutput);
|
|
120
175
|
console.log(` -> ${rootPath} (root entry point)`);
|
|
121
176
|
}
|
|
122
|
-
return { success: true, componentCount: compiled.length, errors: [] };
|
|
177
|
+
return { success: true, componentCount: compiled.length, errors: [], warnings: warningMessages };
|
|
123
178
|
}
|
|
124
179
|
function resolveCustomComponents(nodes, map) {
|
|
125
180
|
for (const node of nodes) {
|
|
@@ -210,8 +265,15 @@ function generateEmbedHeaders(images, sourceDir, outputDir) {
|
|
|
210
265
|
if (!img.embedKey)
|
|
211
266
|
continue;
|
|
212
267
|
const rawSrc = img.src.replace(/^"|"$/g, '');
|
|
213
|
-
|
|
268
|
+
let imagePath = path.resolve(sourceDir, rawSrc);
|
|
214
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
|
+
}
|
|
215
277
|
// Mtime caching: skip if header exists and is newer than image
|
|
216
278
|
if (fs.existsSync(headerPath) && fs.existsSync(imagePath)) {
|
|
217
279
|
const imgStat = fs.statSync(imagePath);
|
|
@@ -221,7 +283,7 @@ function generateEmbedHeaders(images, sourceDir, outputDir) {
|
|
|
221
283
|
}
|
|
222
284
|
}
|
|
223
285
|
if (!fs.existsSync(imagePath)) {
|
|
224
|
-
console.warn(` warning: embedded image not found: ${imagePath}`);
|
|
286
|
+
console.warn(` warning: embedded image not found: ${imagePath} (also tried public/)`);
|
|
225
287
|
continue;
|
|
226
288
|
}
|
|
227
289
|
const imageData = fs.readFileSync(imagePath);
|
|
@@ -239,11 +301,113 @@ function generateEmbedHeaders(images, sourceDir, outputDir) {
|
|
|
239
301
|
console.log(` ${rawSrc} -> ${headerPath} (embed)`);
|
|
240
302
|
}
|
|
241
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
|
+
}
|
|
242
383
|
function detectBoundProps(nodes) {
|
|
243
384
|
const bound = new Set();
|
|
244
385
|
walkNodesForBinding(nodes, bound);
|
|
245
386
|
return bound;
|
|
246
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
|
+
}
|
|
247
411
|
function walkNodesForBinding(nodes, bound) {
|
|
248
412
|
for (const node of nodes) {
|
|
249
413
|
if ('directBind' in node && node.directBind && 'valueExpr' in node) {
|
|
@@ -253,6 +417,10 @@ function walkNodesForBinding(nodes, bound) {
|
|
|
253
417
|
bound.add(propName);
|
|
254
418
|
}
|
|
255
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);
|
|
256
424
|
if (node.kind === 'conditional') {
|
|
257
425
|
walkNodesForBinding(node.body, bound);
|
|
258
426
|
if (node.elseBody)
|
|
@@ -263,10 +431,115 @@ function walkNodesForBinding(nodes, bound) {
|
|
|
263
431
|
}
|
|
264
432
|
}
|
|
265
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
|
+
}
|
|
266
529
|
/**
|
|
267
530
|
* Parse the imx.d.ts in the given directory (if present) and extract
|
|
268
531
|
* all interface declarations as a map from interface name -> field name -> type.
|
|
269
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
|
+
}
|
|
270
543
|
function loadExternalInterfaces(dir) {
|
|
271
544
|
const result = new Map();
|
|
272
545
|
const dtsPath = path.join(dir, 'imx.d.ts');
|
|
@@ -289,20 +562,7 @@ function loadExternalInterfaces(dir) {
|
|
|
289
562
|
fields.set(fieldName, 'callback');
|
|
290
563
|
continue;
|
|
291
564
|
}
|
|
292
|
-
|
|
293
|
-
if (typeText === 'number') {
|
|
294
|
-
fields.set(fieldName, 'float');
|
|
295
|
-
}
|
|
296
|
-
else if (typeText === 'boolean') {
|
|
297
|
-
fields.set(fieldName, 'bool');
|
|
298
|
-
}
|
|
299
|
-
else if (typeText === 'string') {
|
|
300
|
-
fields.set(fieldName, 'string');
|
|
301
|
-
}
|
|
302
|
-
else {
|
|
303
|
-
// Arrays, nested interfaces, etc. treated as opaque (non-scalar)
|
|
304
|
-
fields.set(fieldName, 'string');
|
|
305
|
-
}
|
|
565
|
+
fields.set(fieldName, normalizeExternalPropType(member.type.getText(sf)));
|
|
306
566
|
}
|
|
307
567
|
else if (ts.isMethodSignature(member)) {
|
|
308
568
|
const mName = ts.isIdentifier(member.name) ? member.name.text : '';
|