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/validator.js CHANGED
@@ -1,18 +1,23 @@
1
1
  import ts from 'typescript';
2
- import { HOST_COMPONENTS } from './components.js';
2
+ import { HOST_COMPONENTS, isHostComponent } from './components.js';
3
3
  import { extractImports } from './parser.js';
4
4
  function err(sf, node, msg) {
5
5
  const { line, character } = sf.getLineAndCharacterOfPosition(node.getStart());
6
6
  return { file: sf.fileName, line: line + 1, col: character + 1, message: msg };
7
7
  }
8
+ function warn(sf, node, msg) {
9
+ const { line, character } = sf.getLineAndCharacterOfPosition(node.getStart());
10
+ return { file: sf.fileName, line: line + 1, col: character + 1, message: msg, severity: 'warning' };
11
+ }
8
12
  export function validate(parsed) {
9
13
  const errors = [];
14
+ const warnings = [];
10
15
  const sf = parsed.sourceFile;
11
16
  const func = parsed.component;
12
17
  const customComponents = extractImports(sf);
13
18
  const useStateCalls = [];
14
19
  if (!func || !func.body)
15
- return { errors, customComponents, useStateCalls };
20
+ return { errors, warnings, customComponents, useStateCalls };
16
21
  let slotIndex = 0;
17
22
  for (const stmt of func.body.statements) {
18
23
  if (ts.isVariableStatement(stmt)) {
@@ -29,9 +34,9 @@ export function validate(parsed) {
29
34
  }
30
35
  const returnStmt = func.body.statements.find(ts.isReturnStatement);
31
36
  if (returnStmt && returnStmt.expression) {
32
- validateExpression(returnStmt.expression, sf, customComponents, errors);
37
+ validateExpression(returnStmt.expression, sf, customComponents, errors, warnings);
33
38
  }
34
- return { errors, customComponents, useStateCalls };
39
+ return { errors, warnings, customComponents, useStateCalls };
35
40
  }
36
41
  function isUseStateCall(decl) {
37
42
  if (!decl.initializer || !ts.isCallExpression(decl.initializer))
@@ -65,56 +70,80 @@ function extractUseState(decl, index, sf, errors) {
65
70
  }
66
71
  return { name: nameEl.name.text, setter: setterEl.name.text, initializer: call.arguments[0], index };
67
72
  }
68
- function validateExpression(node, sf, customComponents, errors) {
73
+ function validateExpression(node, sf, customComponents, errors, warnings) {
69
74
  if (ts.isJsxElement(node)) {
70
- validateJsxElement(node, sf, customComponents, errors);
75
+ validateJsxElement(node, sf, customComponents, errors, warnings);
71
76
  }
72
77
  else if (ts.isJsxSelfClosingElement(node)) {
73
- validateJsxTag(node.tagName, node, sf, customComponents, errors);
78
+ validateJsxTag(node.tagName, node, sf, customComponents, warnings);
74
79
  validateJsxAttributes(node.attributes, node.tagName, sf, errors);
75
80
  }
76
81
  else if (ts.isJsxFragment(node)) {
77
82
  for (const child of node.children)
78
- validateExpression(child, sf, customComponents, errors);
83
+ validateExpression(child, sf, customComponents, errors, warnings);
79
84
  }
80
85
  else if (ts.isParenthesizedExpression(node)) {
81
- validateExpression(node.expression, sf, customComponents, errors);
86
+ validateExpression(node.expression, sf, customComponents, errors, warnings);
82
87
  }
83
88
  else if (ts.isConditionalExpression(node)) {
84
- validateExpression(node.whenTrue, sf, customComponents, errors);
85
- validateExpression(node.whenFalse, sf, customComponents, errors);
89
+ validateExpression(node.whenTrue, sf, customComponents, errors, warnings);
90
+ validateExpression(node.whenFalse, sf, customComponents, errors, warnings);
86
91
  }
87
92
  else if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) {
88
- validateExpression(node.right, sf, customComponents, errors);
93
+ validateExpression(node.right, sf, customComponents, errors, warnings);
89
94
  }
90
95
  else if (ts.isJsxExpression(node) && node.expression) {
91
- validateExpression(node.expression, sf, customComponents, errors);
96
+ validateExpression(node.expression, sf, customComponents, errors, warnings);
92
97
  }
93
98
  else if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === 'map') {
94
99
  const callback = node.arguments[0];
95
100
  if (callback && (ts.isArrowFunction(callback) || ts.isFunctionExpression(callback))) {
101
+ let mapBody;
96
102
  if (ts.isBlock(callback.body)) {
97
103
  const ret = callback.body.statements.find(ts.isReturnStatement);
98
- if (ret?.expression)
99
- validateExpression(ret.expression, sf, customComponents, errors);
104
+ mapBody = ret?.expression;
100
105
  }
101
106
  else if (callback.body) {
102
- validateExpression(callback.body, sf, customComponents, errors);
107
+ mapBody = callback.body;
108
+ }
109
+ if (mapBody) {
110
+ validateExpression(mapBody, sf, customComponents, errors, warnings);
111
+ if (!hasIDWrapper(mapBody)) {
112
+ warnings.push(warn(sf, node, 'Items in .map() should be wrapped in <ID scope={i}> to avoid ImGui ID conflicts'));
113
+ }
103
114
  }
104
115
  }
105
116
  }
106
117
  }
107
- function validateJsxElement(node, sf, customComponents, errors) {
108
- validateJsxTag(node.openingElement.tagName, node, sf, customComponents, errors);
118
+ // Components that handle their own ID scoping (no <ID> wrapper needed in .map())
119
+ const SELF_SCOPED_COMPONENTS = new Set(['ID', 'TableRow', 'TabItem']);
120
+ function hasIDWrapper(expr) {
121
+ if (ts.isParenthesizedExpression(expr))
122
+ return hasIDWrapper(expr.expression);
123
+ if (ts.isJsxElement(expr) && ts.isIdentifier(expr.openingElement.tagName) && SELF_SCOPED_COMPONENTS.has(expr.openingElement.tagName.text))
124
+ return true;
125
+ if (ts.isJsxSelfClosingElement(expr) && ts.isIdentifier(expr.tagName) && SELF_SCOPED_COMPONENTS.has(expr.tagName.text))
126
+ return true;
127
+ return false;
128
+ }
129
+ function validateJsxElement(node, sf, customComponents, errors, warnings) {
130
+ validateJsxTag(node.openingElement.tagName, node, sf, customComponents, warnings);
109
131
  validateJsxAttributes(node.openingElement.attributes, node.openingElement.tagName, sf, errors);
110
132
  for (const child of node.children)
111
- validateExpression(child, sf, customComponents, errors);
133
+ validateExpression(child, sf, customComponents, errors, warnings);
112
134
  }
113
- function validateJsxTag(tagName, node, sf, customComponents, errors) {
135
+ function validateJsxTag(tagName, node, sf, customComponents, warnings) {
114
136
  if (!ts.isIdentifier(tagName))
115
137
  return;
116
- // Host components and imported custom components are validated.
117
- // Unknown elements are treated as native widgets (validated by TypeScript + C++ linker).
138
+ const name = tagName.text;
139
+ // Skip lowercase tags (intrinsic HTML-like elements)
140
+ if (name[0] === name[0].toLowerCase())
141
+ return;
142
+ // Known host component or imported custom component — fine
143
+ if (isHostComponent(name) || customComponents.has(name))
144
+ return;
145
+ // 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.`));
118
147
  }
119
148
  function validateJsxAttributes(attrs, tagName, sf, errors) {
120
149
  if (!ts.isIdentifier(tagName))
package/dist/watch.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  /**
2
2
  * Start watching a directory for .tsx changes and recompile on change.
3
+ * If buildCmd is provided, runs it after each successful compile (for hot reload).
3
4
  */
4
- export declare function startWatch(watchDir: string, outputDir: string): void;
5
+ export declare function startWatch(watchDir: string, outputDir: string, buildCmd?: string): void;
package/dist/watch.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import * as fs from 'node:fs';
2
2
  import * as path from 'node:path';
3
+ import { execSync } from 'node:child_process';
3
4
  import { compile } from './compile.js';
4
5
  /**
5
6
  * Discover all .tsx files in a directory (recursive).
@@ -18,7 +19,7 @@ function discoverTsxFiles(dir) {
18
19
  }
19
20
  return results;
20
21
  }
21
- function runCompile(watchDir, outputDir) {
22
+ function runCompile(watchDir, outputDir, buildCmd) {
22
23
  const files = discoverTsxFiles(watchDir);
23
24
  if (files.length === 0) {
24
25
  console.log('[watch] No .tsx files found in ' + watchDir);
@@ -27,8 +28,21 @@ function runCompile(watchDir, outputDir) {
27
28
  const start = performance.now();
28
29
  const result = compile(files, outputDir);
29
30
  const elapsed = Math.round(performance.now() - start);
31
+ if (result.warnings.length > 0) {
32
+ result.warnings.forEach(w => console.warn(w));
33
+ }
30
34
  if (result.success) {
31
35
  console.log(`[watch] ${result.componentCount} component(s) compiled in ${elapsed}ms`);
36
+ if (buildCmd) {
37
+ console.log(`[watch] running: ${buildCmd}`);
38
+ try {
39
+ execSync(buildCmd, { stdio: 'inherit' });
40
+ console.log('[watch] build succeeded');
41
+ }
42
+ catch {
43
+ console.error('[watch] build failed');
44
+ }
45
+ }
32
46
  }
33
47
  else {
34
48
  result.errors.forEach(e => console.error(e));
@@ -37,13 +51,16 @@ function runCompile(watchDir, outputDir) {
37
51
  }
38
52
  /**
39
53
  * Start watching a directory for .tsx changes and recompile on change.
54
+ * If buildCmd is provided, runs it after each successful compile (for hot reload).
40
55
  */
41
- export function startWatch(watchDir, outputDir) {
56
+ export function startWatch(watchDir, outputDir, buildCmd) {
42
57
  console.log(`[watch] watching ${watchDir} for .tsx changes...`);
43
58
  console.log(`[watch] output: ${outputDir}`);
59
+ if (buildCmd)
60
+ console.log(`[watch] build: ${buildCmd}`);
44
61
  console.log('[watch] press Ctrl+C to stop\n');
45
62
  // Initial compile
46
- runCompile(watchDir, outputDir);
63
+ runCompile(watchDir, outputDir, buildCmd);
47
64
  // Debounce timer
48
65
  let debounceTimer = null;
49
66
  const watcher = fs.watch(watchDir, { recursive: true }, (_event, filename) => {
@@ -54,7 +71,7 @@ export function startWatch(watchDir, outputDir) {
54
71
  clearTimeout(debounceTimer);
55
72
  debounceTimer = setTimeout(() => {
56
73
  console.log(`\n[watch] change detected: ${filename}`);
57
- runCompile(watchDir, outputDir);
74
+ runCompile(watchDir, outputDir, buildCmd);
58
75
  }, 100);
59
76
  });
60
77
  // Clean shutdown on Ctrl+C
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "imxc",
3
- "version": "0.5.4",
3
+ "version": "0.6.1",
4
4
  "description": "Compiler for IMX — compiles React-like .tsx to native Dear ImGui C++",
5
5
  "type": "module",
6
6
  "bin": {
@@ -17,10 +17,8 @@
17
17
  },
18
18
  "keywords": ["imgui", "react", "tsx", "native", "gui", "compiler", "codegen"],
19
19
  "license": "MIT",
20
- "dependencies": {
21
- "typescript": "^5.8.0"
22
- },
23
20
  "devDependencies": {
21
+ "typescript": "^5.8.0",
24
22
  "vitest": "^3.1.0",
25
23
  "@types/node": "^22.0.0"
26
24
  }