imxc 0.6.1 → 0.6.3

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/README.md CHANGED
@@ -5,23 +5,58 @@ Compiler for IMX. Compiles React-like `.tsx` to native Dear ImGui C++ apps.
5
5
  ## Usage
6
6
 
7
7
  ```bash
8
- # Scaffold a new project
8
+ npx imxc --help
9
+ npx imxc --version
10
+
9
11
  npx imxc init myapp
10
12
  cd myapp
11
13
  cmake -B build
12
14
  cmake --build build --config Release
13
15
 
14
- # Add to existing CMake project
15
16
  npx imxc add
16
17
 
17
- # Compile TSX manually
18
18
  imxc App.tsx -o build/generated
19
19
 
20
- # Watch mode
21
20
  imxc watch src -o build/generated
21
+
22
+ npx imxc templates
23
+ ```
24
+
25
+ ## Common Commands
26
+
27
+ ```bash
28
+ # Help
29
+ npx imxc --help
30
+ npx imxc init --help
31
+ npx imxc watch --help
32
+
33
+ # Scaffold a project
34
+ npx imxc init myapp --template=minimal
35
+ npx imxc init myapp --template=async,persistence
36
+
37
+ # Add IMX to an existing project
38
+ npx imxc add
39
+
40
+ # Compile TSX manually
41
+ imxc src/App.tsx src/Counter.tsx -o build/generated
42
+
43
+ # Watch and optionally rebuild
44
+ imxc watch src -o build/generated
45
+ imxc watch src -o build/generated --build "cmake --build build"
46
+
47
+ # List built-in templates
48
+ npx imxc templates
22
49
  ```
23
50
 
24
- 54 components, 5-prop theme system, custom C++ widgets, canvas drawing, drag-drop.
51
+ ## Notes
52
+
53
+ - `imxc watch` prefers `src/App.tsx` as the app entrypoint when present.
54
+ - TypeScript `number` props generate C++ `float`.
55
+ - Declare custom native widgets in `src/imx.d.ts` for type checking.
56
+
57
+ ## Features
58
+
59
+ ~98 components, 5-prop theme system, custom C++ widgets, canvas drawing, drag-drop.
25
60
 
26
61
  Requires: Node.js, CMake 3.25+, C++20 compiler.
27
62
 
package/dist/cli.d.ts ADDED
@@ -0,0 +1,19 @@
1
+ import { compile } from './compile.js';
2
+ import { addToProject, initProject } from './init.js';
3
+ import { TEMPLATES, promptProjectName, promptTemplateName } from './templates/index.js';
4
+ import { startWatch } from './watch.js';
5
+ type WriteFn = (text: string) => void;
6
+ export interface CliDeps {
7
+ compile: typeof compile;
8
+ initProject: typeof initProject;
9
+ addToProject: typeof addToProject;
10
+ startWatch: typeof startWatch;
11
+ promptProjectName: typeof promptProjectName;
12
+ promptTemplateName: typeof promptTemplateName;
13
+ templates: typeof TEMPLATES;
14
+ version: string;
15
+ stdout: WriteFn;
16
+ stderr: WriteFn;
17
+ }
18
+ export declare function runCli(argv: string[], deps?: CliDeps): Promise<number>;
19
+ export {};
package/dist/cli.js ADDED
@@ -0,0 +1,256 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import { parseArgs } from 'node:util';
4
+ import { compile } from './compile.js';
5
+ import { addToProject, initProject } from './init.js';
6
+ import { TEMPLATES, promptProjectName, promptTemplateName } from './templates/index.js';
7
+ import { startWatch } from './watch.js';
8
+ const defaultDeps = {
9
+ compile,
10
+ initProject,
11
+ addToProject,
12
+ startWatch,
13
+ promptProjectName,
14
+ promptTemplateName,
15
+ templates: TEMPLATES,
16
+ version: JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf-8')).version,
17
+ stdout: text => process.stdout.write(text),
18
+ stderr: text => process.stderr.write(text),
19
+ };
20
+ function isHelpFlag(arg) {
21
+ return arg === '--help' || arg === '-h';
22
+ }
23
+ function isVersionFlag(arg) {
24
+ return arg === '--version' || arg === '-v';
25
+ }
26
+ function templateList(templates) {
27
+ return templates.map(t => ` ${t.name} - ${t.description}`).join('\n');
28
+ }
29
+ function mainHelp(templates) {
30
+ return [
31
+ 'imxc - compile IMX .tsx into native Dear ImGui C++.',
32
+ '',
33
+ 'Usage:',
34
+ ' imxc <input.tsx ...> -o <output-dir>',
35
+ ' imxc init [project-dir] [--template=<name[,name,...]>]',
36
+ ' imxc add [project-dir]',
37
+ ' imxc watch <dir> -o <output-dir> [--build <cmd>]',
38
+ ' imxc templates',
39
+ ' imxc help [command]',
40
+ ' imxc --version',
41
+ '',
42
+ 'Notes:',
43
+ ' Root component in explicit builds is the first .tsx input you pass.',
44
+ ' Watch mode chooses the root in this order: src/App.tsx, App.tsx, then first file alphabetically.',
45
+ ' Declare native widgets in src/imx.d.ts to enable typing and suppress unknown-component warnings.',
46
+ '',
47
+ 'Templates:',
48
+ templateList(templates),
49
+ '',
50
+ 'First project:',
51
+ ' npx imxc init myapp --template=minimal',
52
+ ' cd myapp',
53
+ ' cmake -B build',
54
+ ' cmake --build build',
55
+ '',
56
+ ].join('\n');
57
+ }
58
+ function initHelp(templates) {
59
+ return [
60
+ 'Usage: imxc init [project-dir] [--template=<name[,name,...]>]',
61
+ '',
62
+ 'Creates a new IMX project scaffold.',
63
+ 'If no project dir is provided, imxc prompts for one.',
64
+ 'If no template is provided, imxc opens the interactive template selector.',
65
+ '',
66
+ 'Available templates:',
67
+ templateList(templates),
68
+ '',
69
+ 'Examples:',
70
+ ' imxc init myapp',
71
+ ' imxc init myapp --template=minimal',
72
+ ' imxc init myapp --template=async,persistence',
73
+ '',
74
+ ].join('\n');
75
+ }
76
+ function addHelp() {
77
+ return [
78
+ 'Usage: imxc add [project-dir]',
79
+ '',
80
+ 'Adds IMX compiler integration to an existing CMake project.',
81
+ 'Defaults to the current directory when no project dir is provided.',
82
+ '',
83
+ ].join('\n');
84
+ }
85
+ function watchHelp() {
86
+ return [
87
+ 'Usage: imxc watch <dir> -o <output-dir> [--build <cmd>]',
88
+ '',
89
+ 'Watches a directory recursively for .tsx changes and recompiles on change.',
90
+ 'Root selection order is: src/App.tsx, App.tsx, then first file alphabetically.',
91
+ 'If --build is provided, imxc runs the build command after each successful compile.',
92
+ '',
93
+ 'Examples:',
94
+ ' imxc watch src -o build/generated',
95
+ ' imxc watch src -o build/generated --build "cmake --build build"',
96
+ '',
97
+ ].join('\n');
98
+ }
99
+ function templatesHelp(templates) {
100
+ return [
101
+ 'Usage: imxc templates',
102
+ '',
103
+ 'Lists all built-in project templates.',
104
+ '',
105
+ 'Templates:',
106
+ templateList(templates),
107
+ '',
108
+ ].join('\n');
109
+ }
110
+ function helpText(command, templates) {
111
+ switch (command) {
112
+ case 'init': return initHelp(templates);
113
+ case 'add': return addHelp();
114
+ case 'watch': return watchHelp();
115
+ case 'templates': return templatesHelp(templates);
116
+ default: return mainHelp(templates);
117
+ }
118
+ }
119
+ function formatParseError(error) {
120
+ if (error && typeof error === 'object' && 'message' in error && typeof error.message === 'string') {
121
+ return error.message;
122
+ }
123
+ return 'Invalid command line arguments.';
124
+ }
125
+ function asOptionalString(value) {
126
+ return typeof value === 'string' ? value : undefined;
127
+ }
128
+ function safeParseArgs(config, deps, help) {
129
+ try {
130
+ return parseArgs(config);
131
+ }
132
+ catch (error) {
133
+ deps.stderr(`imxc: ${formatParseError(error)}\n\n${help}`);
134
+ return null;
135
+ }
136
+ }
137
+ export async function runCli(argv, deps = defaultDeps) {
138
+ const [command, ...rest] = argv;
139
+ if (!command) {
140
+ deps.stdout(mainHelp(deps.templates));
141
+ return 0;
142
+ }
143
+ if (isHelpFlag(command)) {
144
+ deps.stdout(mainHelp(deps.templates));
145
+ return 0;
146
+ }
147
+ if (isVersionFlag(command)) {
148
+ deps.stdout(`${deps.version}\n`);
149
+ return 0;
150
+ }
151
+ if (command === 'help') {
152
+ deps.stdout(helpText(rest[0], deps.templates));
153
+ return 0;
154
+ }
155
+ if (command === 'templates') {
156
+ if (isHelpFlag(rest[0])) {
157
+ deps.stdout(templatesHelp(deps.templates));
158
+ }
159
+ else if (rest.length > 0) {
160
+ deps.stderr(`imxc: templates does not accept additional arguments.\n\n${templatesHelp(deps.templates)}`);
161
+ return 1;
162
+ }
163
+ else {
164
+ deps.stdout(`Available templates:\n\n${templateList(deps.templates)}\n\nUse "imxc init <project-dir> --template=<name[,name,...]>" to scaffold one.\n`);
165
+ }
166
+ return 0;
167
+ }
168
+ if (command === 'init') {
169
+ if (isHelpFlag(rest[0])) {
170
+ deps.stdout(initHelp(deps.templates));
171
+ return 0;
172
+ }
173
+ const parsed = safeParseArgs({
174
+ args: rest,
175
+ allowPositionals: true,
176
+ options: { template: { type: 'string', short: 't' } },
177
+ }, deps, initHelp(deps.templates));
178
+ if (!parsed)
179
+ return 1;
180
+ let dir = parsed.positionals[0];
181
+ if (!dir) {
182
+ dir = await deps.promptProjectName();
183
+ }
184
+ const absDir = path.resolve(dir);
185
+ const templateName = asOptionalString(parsed.values.template) ?? await deps.promptTemplateName();
186
+ deps.initProject(absDir, path.basename(absDir), templateName);
187
+ return 0;
188
+ }
189
+ if (command === 'add') {
190
+ if (isHelpFlag(rest[0])) {
191
+ deps.stdout(addHelp());
192
+ return 0;
193
+ }
194
+ const parsed = safeParseArgs({
195
+ args: rest,
196
+ allowPositionals: true,
197
+ options: {},
198
+ }, deps, addHelp());
199
+ if (!parsed)
200
+ return 1;
201
+ const dir = parsed.positionals[0] ?? '.';
202
+ deps.addToProject(path.resolve(dir));
203
+ return 0;
204
+ }
205
+ if (command === 'watch') {
206
+ if (isHelpFlag(rest[0])) {
207
+ deps.stdout(watchHelp());
208
+ return 0;
209
+ }
210
+ if (!rest[0]) {
211
+ deps.stderr(`imxc: missing watch directory.\n\n${watchHelp()}`);
212
+ return 1;
213
+ }
214
+ const watchDir = rest[0];
215
+ const parsed = safeParseArgs({
216
+ args: rest.slice(1),
217
+ allowPositionals: false,
218
+ options: {
219
+ output: { type: 'string', short: 'o' },
220
+ build: { type: 'string', short: 'b' },
221
+ },
222
+ }, deps, watchHelp());
223
+ if (!parsed)
224
+ return 1;
225
+ const outputDir = asOptionalString(parsed.values.output) ?? '.';
226
+ const buildCmd = asOptionalString(parsed.values.build);
227
+ deps.startWatch(path.resolve(watchDir), path.resolve(outputDir), buildCmd);
228
+ return 0;
229
+ }
230
+ if (command.startsWith('-')) {
231
+ deps.stderr(`imxc: unknown option '${command}'.\n\n${mainHelp(deps.templates)}`);
232
+ return 1;
233
+ }
234
+ const parsed = safeParseArgs({
235
+ args: argv,
236
+ allowPositionals: true,
237
+ options: { output: { type: 'string', short: 'o' } },
238
+ }, deps, mainHelp(deps.templates));
239
+ if (!parsed)
240
+ return 1;
241
+ if (parsed.positionals.length === 0) {
242
+ deps.stderr(`imxc: no input .tsx files provided.\n\n${mainHelp(deps.templates)}`);
243
+ return 1;
244
+ }
245
+ const outputDir = asOptionalString(parsed.values.output) ?? '.';
246
+ const result = deps.compile(parsed.positionals, outputDir);
247
+ if (result.warnings.length > 0) {
248
+ result.warnings.forEach(w => deps.stderr(w + '\n'));
249
+ }
250
+ if (!result.success) {
251
+ result.errors.forEach(e => deps.stderr(e + '\n'));
252
+ return 1;
253
+ }
254
+ deps.stdout(`imxc: ${result.componentCount} component(s) compiled successfully.\n`);
255
+ return 0;
256
+ }
package/dist/compile.js CHANGED
@@ -49,7 +49,7 @@ export function compile(files, outputDir) {
49
49
  const imports = extractImports(parsed.sourceFile);
50
50
  compiled.push({
51
51
  name: ir.name,
52
- sourceFile: path.basename(file),
52
+ sourceFile: parsed.sourceFile.fileName,
53
53
  sourcePath: file,
54
54
  stateCount: ir.stateSlots.length,
55
55
  bufferCount: ir.bufferCount,
@@ -99,7 +99,7 @@ export function compile(files, outputDir) {
99
99
  }
100
100
  const sharedPropsType = compiled.find(c => c.ir.namedPropsType)?.ir.namedPropsType;
101
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`,
102
+ // When a child declares `speed: number`, but the parent struct has `float speed`,
103
103
  // the bound prop pointer must use the parent's actual type.
104
104
  const resolvedBoundPropTypes = new Map();
105
105
  for (const comp of compiled) {
@@ -478,7 +478,7 @@ function extractNestedPropAccess(expr, bound) {
478
478
  }
479
479
  /**
480
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
481
+ * When a child has `speed: number` but the parent passes props.speed from a struct
482
482
  * where speed is float, the resolved type overrides the child's inferred type.
483
483
  */
484
484
  function resolveChildBoundTypes(nodes, parentFieldTypes, componentMap, extIfaces, result) {
@@ -489,25 +489,7 @@ function resolveChildBoundTypes(nodes, parentFieldTypes, componentMap, extIfaces
489
489
  for (const [propName, valueExpr] of Object.entries(node.props)) {
490
490
  if (!child.boundProps.has(propName) || !valueExpr.startsWith('props.'))
491
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
- }
492
+ const resolvedType = resolveBoundPropType(valueExpr, parentFieldTypes, extIfaces);
511
493
  if (resolvedType) {
512
494
  if (!result.has(node.name))
513
495
  result.set(node.name, new Map());
@@ -526,6 +508,30 @@ function resolveChildBoundTypes(nodes, parentFieldTypes, componentMap, extIfaces
526
508
  }
527
509
  }
528
510
  }
511
+ function resolveBoundPropType(valueExpr, rootFieldTypes, extIfaces) {
512
+ if (!valueExpr.startsWith('props.'))
513
+ return undefined;
514
+ const parts = valueExpr
515
+ .slice(6)
516
+ .split('.')
517
+ .map(part => part.split('[')[0])
518
+ .filter(Boolean);
519
+ if (parts.length === 0)
520
+ return undefined;
521
+ let currentType = rootFieldTypes.get(parts[0]);
522
+ if (!currentType || currentType === 'callback')
523
+ return undefined;
524
+ for (let i = 1; i < parts.length; i++) {
525
+ const iface = extIfaces.get(currentType);
526
+ if (!iface)
527
+ return undefined;
528
+ const nextType = iface.get(parts[i]);
529
+ if (!nextType || nextType === 'callback')
530
+ return undefined;
531
+ currentType = nextType;
532
+ }
533
+ return currentType;
534
+ }
529
535
  /**
530
536
  * Parse the imx.d.ts in the given directory (if present) and extract
531
537
  * all interface declarations as a map from interface name -> field name -> type.
package/dist/index.js CHANGED
@@ -1,83 +1,4 @@
1
1
  #!/usr/bin/env node
2
- import { parseArgs } from 'node:util';
3
- import * as path from 'node:path';
4
- import { initProject, addToProject } from './init.js';
5
- import { compile } from './compile.js';
6
- import { startWatch } from './watch.js';
7
- import { promptProjectName, promptTemplateName, TEMPLATES } from './templates/index.js';
8
- // Handle `imxc templates` — list available templates
9
- if (process.argv[2] === 'templates') {
10
- console.log('Available templates:\n');
11
- TEMPLATES.forEach(t => {
12
- console.log(` ${t.name} — ${t.description}`);
13
- });
14
- console.log('\nUsage: imxc init <project-name> --template=<name>');
15
- console.log('Combine: imxc init <project-name> --template=async,persistence');
16
- process.exit(0);
17
- }
18
- // Handle `imxc init [dir] [--template=<name>]` subcommand
19
- if (process.argv[2] === 'init') {
20
- const { values, positionals } = parseArgs({
21
- args: process.argv.slice(3),
22
- allowPositionals: true,
23
- options: { template: { type: 'string', short: 't' } },
24
- });
25
- let dir = positionals[0];
26
- if (!dir) {
27
- dir = await promptProjectName();
28
- }
29
- const absDir = path.resolve(dir);
30
- const templateName = values.template ?? await promptTemplateName();
31
- initProject(absDir, path.basename(absDir), templateName);
32
- process.exit(0);
33
- }
34
- // Handle `imxc add [dir]` subcommand
35
- if (process.argv[2] === 'add') {
36
- const dir = process.argv[3] ?? '.';
37
- const absDir = path.resolve(dir);
38
- addToProject(absDir);
39
- process.exit(0);
40
- }
41
- // Handle `imxc watch <dir> -o <output-dir>` subcommand
42
- if (process.argv[2] === 'watch') {
43
- const watchDir = process.argv[3];
44
- if (!watchDir) {
45
- console.error('Usage: imxc watch <dir> -o <output-dir>');
46
- process.exit(1);
47
- }
48
- const { values } = parseArgs({
49
- args: process.argv.slice(4),
50
- allowPositionals: false,
51
- options: {
52
- output: { type: 'string', short: 'o' },
53
- build: { type: 'string', short: 'b' },
54
- },
55
- });
56
- const outputDir = values.output ?? '.';
57
- startWatch(path.resolve(watchDir), path.resolve(outputDir), values.build);
58
- }
59
- else {
60
- // Default: build command
61
- const { values, positionals } = parseArgs({
62
- allowPositionals: true,
63
- options: { output: { type: 'string', short: 'o' } },
64
- });
65
- if (positionals.length === 0) {
66
- console.error('Usage: imxc <input.tsx ...> -o <output-dir>');
67
- console.error(' imxc init [project-dir] [--template=<name[,name,...]>]');
68
- console.error(' imxc templates');
69
- console.error(' imxc add [project-dir]');
70
- console.error(' imxc watch <dir> -o <output-dir> [--build <cmd>]');
71
- process.exit(1);
72
- }
73
- const outputDir = values.output ?? '.';
74
- const result = compile(positionals, outputDir);
75
- if (result.warnings.length > 0) {
76
- result.warnings.forEach(w => console.warn(w));
77
- }
78
- if (!result.success) {
79
- result.errors.forEach(e => console.error(e));
80
- process.exit(1);
81
- }
82
- console.log(`imxc: ${result.componentCount} component(s) compiled successfully.`);
83
- }
2
+ import { runCli } from './cli.js';
3
+ const exitCode = await runCli(process.argv.slice(2));
4
+ process.exit(exitCode);
package/dist/init.js CHANGED
@@ -39,7 +39,7 @@ export function addToProject(projectDir) {
39
39
  console.log(' include(FetchContent)');
40
40
  console.log(' FetchContent_Declare(imx');
41
41
  console.log(' GIT_REPOSITORY https://github.com/bgocumlu/imx.git');
42
- console.log(' GIT_TAG v0.6.1');
42
+ console.log(' GIT_TAG v0.6.2');
43
43
  console.log(' )');
44
44
  console.log(' FetchContent_MakeAvailable(imx)');
45
45
  console.log(' include(ImxCompile)');
package/dist/lowering.js CHANGED
@@ -90,7 +90,7 @@ export function lowerComponent(parsed, validation, externalInterfaces) {
90
90
  function normalizePropTypeText(typeText) {
91
91
  const trimmed = typeText.trim().replace(/\s*\|\s*undefined$/, '');
92
92
  if (trimmed === 'number')
93
- return 'int';
93
+ return 'float';
94
94
  if (trimmed === 'boolean')
95
95
  return 'bool';
96
96
  if (trimmed === 'string')
@@ -1215,33 +1215,9 @@ function inferExprType(expr, ctx) {
1215
1215
  // .length maps to .size() in C++ — always int
1216
1216
  if (prop === 'length')
1217
1217
  return 'int';
1218
- // If accessing a direct field of the props param, look up its type
1219
- if (ctx.propsParam && ts.isIdentifier(expr.expression) && expr.expression.text === ctx.propsParam) {
1220
- const ft = ctx.propsFieldTypes.get(prop);
1221
- if (ft && ft !== 'callback') {
1222
- if (ft === 'int' || ft === 'float' || ft === 'bool' || ft === 'string' || ft === 'color' || ft === 'int_array') {
1223
- return ft;
1224
- }
1225
- return 'string';
1226
- }
1227
- }
1228
- // Nested access: props.data.field — resolve through external interfaces
1229
- if (ctx.propsParam && ctx.externalInterfaces && ts.isPropertyAccessExpression(expr.expression)) {
1230
- const mid = expr.expression;
1231
- if (ts.isIdentifier(mid.expression) && mid.expression.text === ctx.propsParam) {
1232
- const midType = ctx.propsFieldTypes.get(mid.name.text);
1233
- if (midType && midType !== 'callback') {
1234
- const iface = ctx.externalInterfaces.get(midType);
1235
- if (iface) {
1236
- const fieldType = iface.get(prop);
1237
- if (fieldType && fieldType !== 'callback') {
1238
- if (fieldType === 'int' || fieldType === 'float' || fieldType === 'bool' || fieldType === 'string' || fieldType === 'color' || fieldType === 'int_array') {
1239
- return fieldType;
1240
- }
1241
- }
1242
- }
1243
- }
1244
- }
1218
+ const resolvedType = resolvePropertyAccessType(expr, ctx);
1219
+ if (resolvedType) {
1220
+ return resolvedType;
1245
1221
  }
1246
1222
  return 'string';
1247
1223
  }
@@ -1275,6 +1251,37 @@ function inferExprType(expr, ctx) {
1275
1251
  }
1276
1252
  return 'int'; // default
1277
1253
  }
1254
+ function resolvePropertyAccessType(expr, ctx) {
1255
+ if (!ctx.propsParam)
1256
+ return null;
1257
+ const parts = [];
1258
+ let current = expr;
1259
+ while (ts.isPropertyAccessExpression(current)) {
1260
+ parts.unshift(current.name.text);
1261
+ current = current.expression;
1262
+ }
1263
+ if (!ts.isIdentifier(current) || current.text !== ctx.propsParam || parts.length === 0) {
1264
+ return null;
1265
+ }
1266
+ let currentType = ctx.propsFieldTypes.get(parts[0]);
1267
+ if (!currentType || currentType === 'callback')
1268
+ return null;
1269
+ for (let i = 1; i < parts.length; i++) {
1270
+ if (!ctx.externalInterfaces)
1271
+ return null;
1272
+ const iface = ctx.externalInterfaces.get(currentType);
1273
+ if (!iface)
1274
+ return null;
1275
+ const nextType = iface.get(parts[i]);
1276
+ if (!nextType || nextType === 'callback')
1277
+ return null;
1278
+ currentType = nextType;
1279
+ }
1280
+ if (currentType === 'int' || currentType === 'float' || currentType === 'bool' || currentType === 'string' || currentType === 'color' || currentType === 'int_array') {
1281
+ return currentType;
1282
+ }
1283
+ return 'string';
1284
+ }
1278
1285
  function lowerListMap(node, body, ctx, loc) {
1279
1286
  const propAccess = node.expression;
1280
1287
  const array = exprToCpp(propAccess.expression, ctx);
package/dist/parser.d.ts CHANGED
@@ -12,5 +12,6 @@ export interface ParseError {
12
12
  message: string;
13
13
  severity?: 'error' | 'warning';
14
14
  }
15
+ export declare function normalizeDisplayPath(filePath: string): string;
15
16
  export declare function parseFile(filePath: string, source: string): ParsedFile;
16
17
  export declare function extractImports(sourceFile: ts.SourceFile): Map<string, string>;
package/dist/parser.js CHANGED
@@ -4,9 +4,19 @@ function formatError(sourceFile, node, message) {
4
4
  const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
5
5
  return { file: sourceFile.fileName, line: line + 1, col: character + 1, message };
6
6
  }
7
+ export function normalizeDisplayPath(filePath) {
8
+ const absolutePath = path.resolve(filePath);
9
+ const relativePath = path.relative(process.cwd(), absolutePath);
10
+ const displayPath = relativePath &&
11
+ !relativePath.startsWith('..') &&
12
+ !path.isAbsolute(relativePath)
13
+ ? relativePath
14
+ : absolutePath;
15
+ return displayPath.replace(/\\/g, '/');
16
+ }
7
17
  export function parseFile(filePath, source) {
8
- const fileName = path.basename(filePath);
9
- const sourceFile = ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
18
+ const displayPath = normalizeDisplayPath(filePath);
19
+ const sourceFile = ts.createSourceFile(displayPath, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
10
20
  const errors = [];
11
21
  let component = null;
12
22
  for (const stmt of sourceFile.statements) {
@@ -18,6 +28,9 @@ export function parseFile(filePath, source) {
18
28
  component = stmt;
19
29
  }
20
30
  }
31
+ else if (ts.isInterfaceDeclaration(stmt)) {
32
+ // allowed
33
+ }
21
34
  else if (ts.isImportDeclaration(stmt)) {
22
35
  // allowed
23
36
  }
@@ -26,9 +39,9 @@ export function parseFile(filePath, source) {
26
39
  }
27
40
  }
28
41
  if (!component && errors.length === 0) {
29
- errors.push({ file: fileName, line: 1, col: 1, message: 'No component function found in file' });
42
+ errors.push({ file: displayPath, line: 1, col: 1, message: 'No component function found in file' });
30
43
  }
31
- return { sourceFile, filePath, component, errors };
44
+ return { sourceFile, filePath: path.resolve(filePath), component, errors };
32
45
  }
33
46
  export function extractImports(sourceFile) {
34
47
  const imports = new Map();
@@ -428,7 +428,7 @@ set(FETCHCONTENT_QUIET OFF)
428
428
  FetchContent_Declare(
429
429
  imx
430
430
  GIT_REPOSITORY https://github.com/bgocumlu/imx.git
431
- GIT_TAG v0.6.1
431
+ GIT_TAG v0.6.3
432
432
  GIT_SHALLOW TRUE
433
433
  GIT_PROGRESS TRUE
434
434
  )
@@ -313,7 +313,7 @@ set(FETCHCONTENT_QUIET OFF)
313
313
  FetchContent_Declare(
314
314
  imx
315
315
  GIT_REPOSITORY https://github.com/bgocumlu/imx.git
316
- GIT_TAG v0.6.1
316
+ GIT_TAG v0.6.3
317
317
  GIT_SHALLOW TRUE
318
318
  GIT_PROGRESS TRUE
319
319
  )
@@ -402,7 +402,7 @@ set(FETCHCONTENT_QUIET OFF)
402
402
  FetchContent_Declare(
403
403
  imx
404
404
  GIT_REPOSITORY ${repoUrl}
405
- GIT_TAG v0.6.1
405
+ GIT_TAG v0.6.3
406
406
  GIT_SHALLOW TRUE
407
407
  GIT_PROGRESS TRUE
408
408
  )
package/dist/validator.js CHANGED
@@ -1,3 +1,5 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
1
3
  import ts from 'typescript';
2
4
  import { HOST_COMPONENTS, isHostComponent } from './components.js';
3
5
  import { extractImports } from './parser.js';
@@ -15,6 +17,7 @@ export function validate(parsed) {
15
17
  const sf = parsed.sourceFile;
16
18
  const func = parsed.component;
17
19
  const customComponents = extractImports(sf);
20
+ const nativeWidgets = loadDeclaredNativeWidgets(path.dirname(parsed.filePath));
18
21
  const useStateCalls = [];
19
22
  if (!func || !func.body)
20
23
  return { errors, warnings, customComponents, useStateCalls };
@@ -34,7 +37,7 @@ export function validate(parsed) {
34
37
  }
35
38
  const returnStmt = func.body.statements.find(ts.isReturnStatement);
36
39
  if (returnStmt && returnStmt.expression) {
37
- validateExpression(returnStmt.expression, sf, customComponents, errors, warnings);
40
+ validateExpression(returnStmt.expression, sf, customComponents, nativeWidgets, errors, warnings);
38
41
  }
39
42
  return { errors, warnings, customComponents, useStateCalls };
40
43
  }
@@ -70,30 +73,30 @@ function extractUseState(decl, index, sf, errors) {
70
73
  }
71
74
  return { name: nameEl.name.text, setter: setterEl.name.text, initializer: call.arguments[0], index };
72
75
  }
73
- function validateExpression(node, sf, customComponents, errors, warnings) {
76
+ function validateExpression(node, sf, customComponents, nativeWidgets, errors, warnings) {
74
77
  if (ts.isJsxElement(node)) {
75
- validateJsxElement(node, sf, customComponents, errors, warnings);
78
+ validateJsxElement(node, sf, customComponents, nativeWidgets, errors, warnings);
76
79
  }
77
80
  else if (ts.isJsxSelfClosingElement(node)) {
78
- validateJsxTag(node.tagName, node, sf, customComponents, warnings);
81
+ validateJsxTag(node.tagName, node, sf, customComponents, nativeWidgets, warnings);
79
82
  validateJsxAttributes(node.attributes, node.tagName, sf, errors);
80
83
  }
81
84
  else if (ts.isJsxFragment(node)) {
82
85
  for (const child of node.children)
83
- validateExpression(child, sf, customComponents, errors, warnings);
86
+ validateExpression(child, sf, customComponents, nativeWidgets, errors, warnings);
84
87
  }
85
88
  else if (ts.isParenthesizedExpression(node)) {
86
- validateExpression(node.expression, sf, customComponents, errors, warnings);
89
+ validateExpression(node.expression, sf, customComponents, nativeWidgets, errors, warnings);
87
90
  }
88
91
  else if (ts.isConditionalExpression(node)) {
89
- validateExpression(node.whenTrue, sf, customComponents, errors, warnings);
90
- validateExpression(node.whenFalse, sf, customComponents, errors, warnings);
92
+ validateExpression(node.whenTrue, sf, customComponents, nativeWidgets, errors, warnings);
93
+ validateExpression(node.whenFalse, sf, customComponents, nativeWidgets, errors, warnings);
91
94
  }
92
95
  else if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) {
93
- validateExpression(node.right, sf, customComponents, errors, warnings);
96
+ validateExpression(node.right, sf, customComponents, nativeWidgets, errors, warnings);
94
97
  }
95
98
  else if (ts.isJsxExpression(node) && node.expression) {
96
- validateExpression(node.expression, sf, customComponents, errors, warnings);
99
+ validateExpression(node.expression, sf, customComponents, nativeWidgets, errors, warnings);
97
100
  }
98
101
  else if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === 'map') {
99
102
  const callback = node.arguments[0];
@@ -107,7 +110,7 @@ function validateExpression(node, sf, customComponents, errors, warnings) {
107
110
  mapBody = callback.body;
108
111
  }
109
112
  if (mapBody) {
110
- validateExpression(mapBody, sf, customComponents, errors, warnings);
113
+ validateExpression(mapBody, sf, customComponents, nativeWidgets, errors, warnings);
111
114
  if (!hasIDWrapper(mapBody)) {
112
115
  warnings.push(warn(sf, node, 'Items in .map() should be wrapped in <ID scope={i}> to avoid ImGui ID conflicts'));
113
116
  }
@@ -126,13 +129,13 @@ function hasIDWrapper(expr) {
126
129
  return true;
127
130
  return false;
128
131
  }
129
- function validateJsxElement(node, sf, customComponents, errors, warnings) {
130
- validateJsxTag(node.openingElement.tagName, node, sf, customComponents, warnings);
132
+ function validateJsxElement(node, sf, customComponents, nativeWidgets, errors, warnings) {
133
+ validateJsxTag(node.openingElement.tagName, node, sf, customComponents, nativeWidgets, warnings);
131
134
  validateJsxAttributes(node.openingElement.attributes, node.openingElement.tagName, sf, errors);
132
135
  for (const child of node.children)
133
- validateExpression(child, sf, customComponents, errors, warnings);
136
+ validateExpression(child, sf, customComponents, nativeWidgets, errors, warnings);
134
137
  }
135
- function validateJsxTag(tagName, node, sf, customComponents, warnings) {
138
+ function validateJsxTag(tagName, node, sf, customComponents, nativeWidgets, warnings) {
136
139
  if (!ts.isIdentifier(tagName))
137
140
  return;
138
141
  const name = tagName.text;
@@ -140,10 +143,10 @@ function validateJsxTag(tagName, node, sf, customComponents, warnings) {
140
143
  if (name[0] === name[0].toLowerCase())
141
144
  return;
142
145
  // Known host component or imported custom component — fine
143
- if (isHostComponent(name) || customComponents.has(name))
146
+ if (isHostComponent(name) || customComponents.has(name) || nativeWidgets.has(name))
144
147
  return;
145
148
  // Unknown uppercase component — warn (may be a native C++ widget)
146
- warnings.push(warn(sf, node, `Unknown component '<${name}>' -- will be treated as a native C++ widget. If this is intentional, you can ignore this warning.`));
149
+ warnings.push(warn(sf, node, `Unknown component '<${name}>' -- will be treated as a native C++ widget. If this is a registered widget, declare it in imx.d.ts to suppress this warning.`));
147
150
  }
148
151
  function validateJsxAttributes(attrs, tagName, sf, errors) {
149
152
  if (!ts.isIdentifier(tagName))
@@ -163,3 +166,20 @@ function validateJsxAttributes(attrs, tagName, sf, errors) {
163
166
  }
164
167
  }
165
168
  }
169
+ function loadDeclaredNativeWidgets(dir) {
170
+ const widgets = new Set();
171
+ const dtsPath = path.join(dir, 'imx.d.ts');
172
+ if (!fs.existsSync(dtsPath))
173
+ return widgets;
174
+ const source = fs.readFileSync(dtsPath, 'utf-8');
175
+ const sf = ts.createSourceFile('imx.d.ts', source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
176
+ for (const stmt of sf.statements) {
177
+ if (!ts.isFunctionDeclaration(stmt) || !stmt.name)
178
+ continue;
179
+ const name = stmt.name.text;
180
+ if (name && name[0] !== name[0].toLowerCase()) {
181
+ widgets.add(name);
182
+ }
183
+ }
184
+ return widgets;
185
+ }
package/dist/watch.d.ts CHANGED
@@ -1,3 +1,13 @@
1
+ /**
2
+ * Discover all .tsx files in a directory (recursive).
3
+ */
4
+ export declare function discoverTsxFiles(dir: string): string[];
5
+ export interface WatchCompileSelection {
6
+ files: string[];
7
+ rootFile: string;
8
+ appCandidates: string[];
9
+ }
10
+ export declare function selectWatchCompileFiles(watchDir: string, discoveredFiles: string[]): WatchCompileSelection;
1
11
  /**
2
12
  * Start watching a directory for .tsx changes and recompile on change.
3
13
  * If buildCmd is provided, runs it after each successful compile (for hot reload).
package/dist/watch.js CHANGED
@@ -5,7 +5,7 @@ import { compile } from './compile.js';
5
5
  /**
6
6
  * Discover all .tsx files in a directory (recursive).
7
7
  */
8
- function discoverTsxFiles(dir) {
8
+ export function discoverTsxFiles(dir) {
9
9
  const results = [];
10
10
  const entries = fs.readdirSync(dir, { withFileTypes: true });
11
11
  for (const entry of entries) {
@@ -19,14 +19,38 @@ function discoverTsxFiles(dir) {
19
19
  }
20
20
  return results;
21
21
  }
22
+ export function selectWatchCompileFiles(watchDir, discoveredFiles) {
23
+ const files = [...discoveredFiles].sort((a, b) => a.localeCompare(b));
24
+ const normalizedDir = path.resolve(watchDir);
25
+ const preferredCandidates = [
26
+ path.join(normalizedDir, 'src', 'App.tsx'),
27
+ path.join(normalizedDir, 'App.tsx'),
28
+ ].map(candidate => path.resolve(candidate));
29
+ const appCandidates = files.filter(file => path.basename(file) === 'App.tsx');
30
+ const rootFile = preferredCandidates.find(candidate => files.includes(candidate)) ??
31
+ files[0];
32
+ if (rootFile !== files[0]) {
33
+ const remaining = files.filter(file => file !== rootFile);
34
+ return { files: [rootFile, ...remaining], rootFile, appCandidates };
35
+ }
36
+ return { files, rootFile, appCandidates };
37
+ }
22
38
  function runCompile(watchDir, outputDir, buildCmd) {
23
- const files = discoverTsxFiles(watchDir);
24
- if (files.length === 0) {
39
+ const discoveredFiles = discoverTsxFiles(watchDir);
40
+ if (discoveredFiles.length === 0) {
25
41
  console.log('[watch] No .tsx files found in ' + watchDir);
26
42
  return;
27
43
  }
44
+ const selection = selectWatchCompileFiles(watchDir, discoveredFiles);
45
+ const rootRelative = path.relative(process.cwd(), selection.rootFile).replace(/\\/g, '/');
46
+ if (selection.appCandidates.length > 1) {
47
+ console.warn(`[watch] multiple App.tsx candidates found; using ${rootRelative} as root`);
48
+ }
49
+ else {
50
+ console.log(`[watch] root: ${rootRelative}`);
51
+ }
28
52
  const start = performance.now();
29
- const result = compile(files, outputDir);
53
+ const result = compile(selection.files, outputDir);
30
54
  const elapsed = Math.round(performance.now() - start);
31
55
  if (result.warnings.length > 0) {
32
56
  result.warnings.forEach(w => console.warn(w));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "imxc",
3
- "version": "0.6.1",
3
+ "version": "0.6.3",
4
4
  "description": "Compiler for IMX — compiles React-like .tsx to native Dear ImGui C++",
5
5
  "type": "module",
6
6
  "bin": {
@@ -17,8 +17,10 @@
17
17
  },
18
18
  "keywords": ["imgui", "react", "tsx", "native", "gui", "compiler", "codegen"],
19
19
  "license": "MIT",
20
+ "dependencies": {
21
+ "typescript": "^5.8.0"
22
+ },
20
23
  "devDependencies": {
21
- "typescript": "^5.8.0",
22
24
  "vitest": "^3.1.0",
23
25
  "@types/node": "^22.0.0"
24
26
  }