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 +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/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,
|
|
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
|
-
|
|
99
|
-
validateExpression(ret.expression, sf, customComponents, errors);
|
|
104
|
+
mapBody = ret?.expression;
|
|
100
105
|
}
|
|
101
106
|
else if (callback.body) {
|
|
102
|
-
|
|
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
|
-
|
|
108
|
-
|
|
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,
|
|
135
|
+
function validateJsxTag(tagName, node, sf, customComponents, warnings) {
|
|
114
136
|
if (!ts.isIdentifier(tagName))
|
|
115
137
|
return;
|
|
116
|
-
|
|
117
|
-
//
|
|
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.
|
|
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
|
}
|