imxc 0.5.4 → 0.6.1

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 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 && importedComp.hasProps) {
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
- // Only generate a header for inline props (not for named interface types —
101
- // those are declared in the user's main.cpp)
102
- if (comp.hasProps && !comp.ir.namedPropsType) {
103
- const headerOutput = emitComponentHeader(comp.ir, comp.sourceFile, comp.boundProps);
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
- const imagePath = path.resolve(sourceDir, rawSrc);
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
- const typeText = member.type.getText(sf);
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 : '';