imxc 0.1.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/components.d.ts +12 -0
- package/dist/components.js +208 -0
- package/dist/emitter.d.ts +12 -0
- package/dist/emitter.js +716 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +119 -0
- package/dist/ir.d.ts +193 -0
- package/dist/ir.js +1 -0
- package/dist/lowering.d.ts +17 -0
- package/dist/lowering.js +788 -0
- package/dist/parser.d.ts +15 -0
- package/dist/parser.js +47 -0
- package/dist/validator.d.ts +14 -0
- package/dist/validator.js +138 -0
- package/package.json +27 -0
package/dist/parser.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
export interface ParsedFile {
|
|
3
|
+
sourceFile: ts.SourceFile;
|
|
4
|
+
filePath: string;
|
|
5
|
+
component: ts.FunctionDeclaration | null;
|
|
6
|
+
errors: ParseError[];
|
|
7
|
+
}
|
|
8
|
+
export interface ParseError {
|
|
9
|
+
file: string;
|
|
10
|
+
line: number;
|
|
11
|
+
col: number;
|
|
12
|
+
message: string;
|
|
13
|
+
}
|
|
14
|
+
export declare function parseIgxFile(filePath: string, source: string): ParsedFile;
|
|
15
|
+
export declare function extractImports(sourceFile: ts.SourceFile): Map<string, string>;
|
package/dist/parser.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
function formatError(sourceFile, node, message) {
|
|
4
|
+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
|
|
5
|
+
return { file: sourceFile.fileName, line: line + 1, col: character + 1, message };
|
|
6
|
+
}
|
|
7
|
+
export function parseIgxFile(filePath, source) {
|
|
8
|
+
const fileName = path.basename(filePath);
|
|
9
|
+
const sourceFile = ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX);
|
|
10
|
+
const errors = [];
|
|
11
|
+
let component = null;
|
|
12
|
+
for (const stmt of sourceFile.statements) {
|
|
13
|
+
if (ts.isFunctionDeclaration(stmt) && stmt.name) {
|
|
14
|
+
if (component !== null) {
|
|
15
|
+
errors.push(formatError(sourceFile, stmt, 'Only one component function per .igx file is supported'));
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
component = stmt;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
else if (ts.isImportDeclaration(stmt)) {
|
|
22
|
+
// allowed
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
errors.push(formatError(sourceFile, stmt, `Unsupported top-level statement: ${ts.SyntaxKind[stmt.kind]}`));
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (!component && errors.length === 0) {
|
|
29
|
+
errors.push({ file: fileName, line: 1, col: 1, message: 'No component function found in file' });
|
|
30
|
+
}
|
|
31
|
+
return { sourceFile, filePath, component, errors };
|
|
32
|
+
}
|
|
33
|
+
export function extractImports(sourceFile) {
|
|
34
|
+
const imports = new Map();
|
|
35
|
+
for (const stmt of sourceFile.statements) {
|
|
36
|
+
if (ts.isImportDeclaration(stmt) && stmt.importClause) {
|
|
37
|
+
const moduleSpecifier = stmt.moduleSpecifier.text;
|
|
38
|
+
const bindings = stmt.importClause.namedBindings;
|
|
39
|
+
if (bindings && ts.isNamedImports(bindings)) {
|
|
40
|
+
for (const spec of bindings.elements) {
|
|
41
|
+
imports.set(spec.name.text, moduleSpecifier);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return imports;
|
|
47
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
import type { ParsedFile, ParseError } from './parser.js';
|
|
3
|
+
export interface ValidationResult {
|
|
4
|
+
errors: ParseError[];
|
|
5
|
+
customComponents: Map<string, string>;
|
|
6
|
+
useStateCalls: UseStateInfo[];
|
|
7
|
+
}
|
|
8
|
+
export interface UseStateInfo {
|
|
9
|
+
name: string;
|
|
10
|
+
setter: string;
|
|
11
|
+
initializer: ts.Expression;
|
|
12
|
+
index: number;
|
|
13
|
+
}
|
|
14
|
+
export declare function validate(parsed: ParsedFile): ValidationResult;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import ts from 'typescript';
|
|
2
|
+
import { HOST_COMPONENTS, isHostComponent } from './components.js';
|
|
3
|
+
import { extractImports } from './parser.js';
|
|
4
|
+
function err(sf, node, msg) {
|
|
5
|
+
const { line, character } = sf.getLineAndCharacterOfPosition(node.getStart());
|
|
6
|
+
return { file: sf.fileName, line: line + 1, col: character + 1, message: msg };
|
|
7
|
+
}
|
|
8
|
+
export function validate(parsed) {
|
|
9
|
+
const errors = [];
|
|
10
|
+
const sf = parsed.sourceFile;
|
|
11
|
+
const func = parsed.component;
|
|
12
|
+
const customComponents = extractImports(sf);
|
|
13
|
+
const useStateCalls = [];
|
|
14
|
+
if (!func || !func.body)
|
|
15
|
+
return { errors, customComponents, useStateCalls };
|
|
16
|
+
let slotIndex = 0;
|
|
17
|
+
for (const stmt of func.body.statements) {
|
|
18
|
+
if (ts.isVariableStatement(stmt)) {
|
|
19
|
+
for (const decl of stmt.declarationList.declarations) {
|
|
20
|
+
if (isUseStateCall(decl)) {
|
|
21
|
+
const info = extractUseState(decl, slotIndex, sf, errors);
|
|
22
|
+
if (info) {
|
|
23
|
+
useStateCalls.push(info);
|
|
24
|
+
slotIndex++;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const returnStmt = func.body.statements.find(ts.isReturnStatement);
|
|
31
|
+
if (returnStmt && returnStmt.expression) {
|
|
32
|
+
validateExpression(returnStmt.expression, sf, customComponents, errors);
|
|
33
|
+
}
|
|
34
|
+
return { errors, customComponents, useStateCalls };
|
|
35
|
+
}
|
|
36
|
+
function isUseStateCall(decl) {
|
|
37
|
+
if (!decl.initializer || !ts.isCallExpression(decl.initializer))
|
|
38
|
+
return false;
|
|
39
|
+
const callee = decl.initializer.expression;
|
|
40
|
+
return ts.isIdentifier(callee) && callee.text === 'useState';
|
|
41
|
+
}
|
|
42
|
+
function extractUseState(decl, index, sf, errors) {
|
|
43
|
+
const call = decl.initializer;
|
|
44
|
+
if (!ts.isArrayBindingPattern(decl.name)) {
|
|
45
|
+
errors.push(err(sf, decl, 'useState must use array destructuring'));
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
const elements = decl.name.elements;
|
|
49
|
+
if (elements.length !== 2) {
|
|
50
|
+
errors.push(err(sf, decl, 'useState destructuring must have exactly 2 elements'));
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
const nameEl = elements[0], setterEl = elements[1];
|
|
54
|
+
if (!ts.isBindingElement(nameEl) || !ts.isIdentifier(nameEl.name)) {
|
|
55
|
+
errors.push(err(sf, nameEl, 'First useState element must be an identifier'));
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
if (!ts.isBindingElement(setterEl) || !ts.isIdentifier(setterEl.name)) {
|
|
59
|
+
errors.push(err(sf, setterEl, 'Second useState element must be an identifier'));
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
if (call.arguments.length !== 1) {
|
|
63
|
+
errors.push(err(sf, call, 'useState requires exactly 1 argument'));
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
return { name: nameEl.name.text, setter: setterEl.name.text, initializer: call.arguments[0], index };
|
|
67
|
+
}
|
|
68
|
+
function validateExpression(node, sf, customComponents, errors) {
|
|
69
|
+
if (ts.isJsxElement(node)) {
|
|
70
|
+
validateJsxElement(node, sf, customComponents, errors);
|
|
71
|
+
}
|
|
72
|
+
else if (ts.isJsxSelfClosingElement(node)) {
|
|
73
|
+
validateJsxTag(node.tagName, node, sf, customComponents, errors);
|
|
74
|
+
validateJsxAttributes(node.attributes, node.tagName, sf, errors);
|
|
75
|
+
}
|
|
76
|
+
else if (ts.isJsxFragment(node)) {
|
|
77
|
+
for (const child of node.children)
|
|
78
|
+
validateExpression(child, sf, customComponents, errors);
|
|
79
|
+
}
|
|
80
|
+
else if (ts.isParenthesizedExpression(node)) {
|
|
81
|
+
validateExpression(node.expression, sf, customComponents, errors);
|
|
82
|
+
}
|
|
83
|
+
else if (ts.isConditionalExpression(node)) {
|
|
84
|
+
validateExpression(node.whenTrue, sf, customComponents, errors);
|
|
85
|
+
validateExpression(node.whenFalse, sf, customComponents, errors);
|
|
86
|
+
}
|
|
87
|
+
else if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.AmpersandAmpersandToken) {
|
|
88
|
+
validateExpression(node.right, sf, customComponents, errors);
|
|
89
|
+
}
|
|
90
|
+
else if (ts.isJsxExpression(node) && node.expression) {
|
|
91
|
+
validateExpression(node.expression, sf, customComponents, errors);
|
|
92
|
+
}
|
|
93
|
+
else if (ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) && node.expression.name.text === 'map') {
|
|
94
|
+
const callback = node.arguments[0];
|
|
95
|
+
if (callback && (ts.isArrowFunction(callback) || ts.isFunctionExpression(callback))) {
|
|
96
|
+
if (ts.isBlock(callback.body)) {
|
|
97
|
+
const ret = callback.body.statements.find(ts.isReturnStatement);
|
|
98
|
+
if (ret?.expression)
|
|
99
|
+
validateExpression(ret.expression, sf, customComponents, errors);
|
|
100
|
+
}
|
|
101
|
+
else if (callback.body) {
|
|
102
|
+
validateExpression(callback.body, sf, customComponents, errors);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function validateJsxElement(node, sf, customComponents, errors) {
|
|
108
|
+
validateJsxTag(node.openingElement.tagName, node, sf, customComponents, errors);
|
|
109
|
+
validateJsxAttributes(node.openingElement.attributes, node.openingElement.tagName, sf, errors);
|
|
110
|
+
for (const child of node.children)
|
|
111
|
+
validateExpression(child, sf, customComponents, errors);
|
|
112
|
+
}
|
|
113
|
+
function validateJsxTag(tagName, node, sf, customComponents, errors) {
|
|
114
|
+
if (!ts.isIdentifier(tagName))
|
|
115
|
+
return;
|
|
116
|
+
const name = tagName.text;
|
|
117
|
+
if (!isHostComponent(name) && !customComponents.has(name)) {
|
|
118
|
+
errors.push(err(sf, node, `Unknown component: <${name}>`));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function validateJsxAttributes(attrs, tagName, sf, errors) {
|
|
122
|
+
if (!ts.isIdentifier(tagName))
|
|
123
|
+
return;
|
|
124
|
+
const name = tagName.text;
|
|
125
|
+
const def = HOST_COMPONENTS[name];
|
|
126
|
+
if (!def)
|
|
127
|
+
return;
|
|
128
|
+
const presentProps = new Set();
|
|
129
|
+
for (const attr of attrs.properties) {
|
|
130
|
+
if (ts.isJsxAttribute(attr) && attr.name && ts.isIdentifier(attr.name))
|
|
131
|
+
presentProps.add(attr.name.text);
|
|
132
|
+
}
|
|
133
|
+
for (const [propName, propDef] of Object.entries(def.props)) {
|
|
134
|
+
if (propDef.required && !presentProps.has(propName)) {
|
|
135
|
+
errors.push(err(sf, attrs, `<${name}> requires prop '${propName}'`));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "imxc",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Compiler for ReImGui — compiles React-like .tsx to native Dear ImGui C++",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"imxc": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist"
|
|
11
|
+
],
|
|
12
|
+
"scripts": {
|
|
13
|
+
"build": "tsc",
|
|
14
|
+
"test": "vitest run",
|
|
15
|
+
"test:watch": "vitest",
|
|
16
|
+
"prepublishOnly": "npm run build"
|
|
17
|
+
},
|
|
18
|
+
"keywords": ["imgui", "react", "tsx", "native", "gui", "compiler", "codegen"],
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"typescript": "^5.8.0"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"vitest": "^3.1.0",
|
|
25
|
+
"@types/node": "^22.0.0"
|
|
26
|
+
}
|
|
27
|
+
}
|