Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
jsx support
  • Loading branch information
cs01 committed Feb 24, 2026
commit 354c25b70debd713fd8ccf89550324713a09853c
4 changes: 2 additions & 2 deletions c_bridges/treesitter-bridge.c
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,13 @@
#include <stdint.h>
#include <stdbool.h>

extern TSLanguage *tree_sitter_typescript(void);
extern TSLanguage *tree_sitter_tsx(void);
extern void *GC_malloc_uncollectable(size_t size);
extern void *GC_malloc_atomic(size_t size);

TSTree *__ts_parse_source(const char *source, uint32_t length) {
TSParser *parser = ts_parser_new();
TSLanguage *lang = tree_sitter_typescript();
TSLanguage *lang = tree_sitter_tsx();
ts_parser_set_language(parser, lang);
return ts_parser_parse_string(parser, NULL, source, length);
}
Expand Down
2 changes: 2 additions & 0 deletions src/chad-native.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,8 @@ let outputFile: string = ".build/" + inputForOutput;
const explicitOutput = parser.getOption("output");
if (explicitOutput.length > 0) {
outputFile = explicitOutput;
} else if (inputForOutput.substr(inputForOutput.length - 4) === ".tsx") {
outputFile = ".build/" + inputForOutput.substr(0, inputForOutput.length - 4);
} else if (inputForOutput.substr(inputForOutput.length - 3) === ".ts") {
outputFile = ".build/" + inputForOutput.substr(0, inputForOutput.length - 3);
} else if (inputForOutput.substr(inputForOutput.length - 3) === ".js") {
Expand Down
32 changes: 28 additions & 4 deletions src/compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,10 @@ const PICOHTTPPARSER_PATH = process.env.CHADSCRIPT_PICOHTTPPARSER_PATH || "./ven
const YYJSON_PATH = process.env.CHADSCRIPT_YYJSON_PATH || "./vendor/yyjson";
const LIBUV_PATH = process.env.CHADSCRIPT_LIBUV_PATH || "./vendor/libuv/build";
const TREESITTER_LIB_PATH = process.env.CHADSCRIPT_TREESITTER_PATH || "./vendor/tree-sitter";
const TREESITTER_TS_PATH = "node_modules/tree-sitter-typescript/typescript/src";
// TSX grammar is a strict superset of TypeScript — all .ts code parses identically.
// The only difference: <Type>expr angle-bracket assertions become JSX, but ChadScript
// uses `as Type` so there's no impact on existing code.
const TREESITTER_TS_PATH = "node_modules/tree-sitter-typescript/tsx/src";

// ============================================
// MAIN COMPILER DRIVER
Expand Down Expand Up @@ -220,13 +223,13 @@ export function compile(

// Create TypeScript type checker if compiling .ts files
let typeChecker: TypeChecker | null = null;
if (inputFile.endsWith(".ts")) {
if (inputFile.endsWith(".ts") || inputFile.endsWith(".tsx")) {
try {
const files: { filename: string; code: string }[] = [];
for (let fci = 0; fci < fileContentKeys.length; fci++) {
const filename = fileContentKeys[fci];
const code = fileContentValues[fci];
if (filename.endsWith(".ts")) {
if (filename.endsWith(".ts") || filename.endsWith(".tsx")) {
files.push({ filename, code });
}
}
Expand Down Expand Up @@ -747,12 +750,30 @@ function resolveImportPath(fromFile: string, importSource: string): string {
const dir = path.dirname(fromFile);
const resolved = path.resolve(dir, importSource);

// If the import has .js extension, prefer .ts source over compiled .js
// If the import has .js extension, prefer .ts/.tsx source over compiled .js
if (importSource.endsWith(".js")) {
const tsPath = resolved.replace(/\.js$/, ".ts");
if (fs.existsSync(tsPath)) {
return tsPath;
}
const tsxPath = resolved.replace(/\.js$/, ".tsx");
if (fs.existsSync(tsxPath)) {
return tsxPath;
}
}

// Extensionless imports: try .ts then .tsx
if (
!importSource.endsWith(".ts") &&
!importSource.endsWith(".tsx") &&
!importSource.endsWith(".js")
) {
if (fs.existsSync(resolved + ".ts")) {
return resolved + ".ts";
}
if (fs.existsSync(resolved + ".tsx")) {
return resolved + ".tsx";
}
}

return resolved;
Expand All @@ -770,9 +791,12 @@ function resolveNodeModule(fromFile: string, packageName: string): string | null
const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8"));
const entryPoints = [
pkgJson.main?.replace(/\.js$/, ".ts"),
pkgJson.main?.replace(/\.js$/, ".tsx"),
pkgJson.main?.replace(/\.js$/, ""),
"index.ts",
"index.tsx",
"src/index.ts",
"src/index.tsx",
].filter(Boolean);

for (const entry of entryPoints) {
Expand Down
10 changes: 9 additions & 1 deletion src/native-compiler-lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -578,12 +578,16 @@ export function resolveImportPath(fromFile: string, importSource: string): strin
const dir = path.dirname(fromFile);
const resolved = path.resolve(dir + "/" + importSource);

// Prefer .ts source over compiled .js
// Prefer .ts/.tsx source over compiled .js
if (importSource.substr(importSource.length - 3) === ".js") {
const tsPath = resolved.substr(0, resolved.length - 3) + ".ts";
if (fs.existsSync(tsPath)) {
return tsPath;
}
const tsxPath = resolved.substr(0, resolved.length - 3) + ".tsx";
if (fs.existsSync(tsxPath)) {
return tsxPath;
}
}

if (fs.existsSync(resolved)) {
Expand All @@ -594,6 +598,10 @@ export function resolveImportPath(fromFile: string, importSource: string): strin
return resolved + ".ts";
}

if (fs.existsSync(resolved + ".tsx")) {
return resolved + ".tsx";
}

if (fs.existsSync(resolved + ".js")) {
return resolved + ".js";
}
Expand Down
133 changes: 133 additions & 0 deletions src/parser-native/transformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -483,11 +483,144 @@ function transformExpression(node: TreeSitterNode): Expression {
case "typeof_expression":
return transformTypeofExpression(node);

// JSX desugaring — convert JSX syntax to createElement() calls
case "jsx_element":
return transformJsxElementNative(node);

case "jsx_self_closing_element":
return transformJsxSelfClosingElementNative(node);

case "jsx_expression":
// Bare JSX expression container — unwrap to the inner expression
const jsxInner = getNamedChild(node, 0);
return jsxInner ? transformExpression(jsxInner) : { type: "variable", name: "undefined" };

default:
return { type: "variable", name: "undefined" };
}
}

// ============================================
// JSX DESUGARING (native parser)
// Mirrors the TS-API parser's JSX desugaring.
// Tree-sitter TSX grammar node types:
// jsx_element: has open_tag (jsx_opening_element) and close_tag (jsx_closing_element)
// jsx_self_closing_element: has name + attributes, no children
// jsx_opening_element: has name field (absent for fragments) + attribute fields
// jsx_text: raw text content between tags
// jsx_expression: {expr} containers
// ============================================

function makeJsxCallNode(tagName: string, props: Expression, children: Expression): CallNode {
// Build args with push() — the semantic analyzer treats push()-built arrays as
// homogeneous (all Expression) whereas inline array literals with different shapes
// trigger "mixed array types" errors during self-hosting.
const args: Expression[] = [];
args.push({ type: "string", value: tagName });
args.push(props);
args.push(children);
return { type: "call", name: "createElement", args };
}

function transformJsxElementNative(node: TreeSitterNode): CallNode {
const openTag = getChildByFieldName(node, "open_tag");
let tagName = "Fragment";
let props: Expression = { type: "object", properties: [] };

if (openTag && !(openTag as NodeBase).isNull) {
const nameNode = getChildByFieldName(openTag, "name");
if (nameNode && !(nameNode as NodeBase).isNull) {
tagName = (nameNode as NodeBase).text;
}
// else: no name field means this is a fragment (<>...</>)
props = transformJsxAttributesNative(openTag);
}

const children = transformJsxChildrenNative(node);
return makeJsxCallNode(tagName, props, children);
}

function transformJsxSelfClosingElementNative(node: TreeSitterNode): CallNode {
const nameNode = getChildByFieldName(node, "name");
const tagName =
nameNode && !(nameNode as NodeBase).isNull ? (nameNode as NodeBase).text : "Fragment";
const props: Expression = transformJsxAttributesNative(node);
const emptyChildren: Expression = { type: "array", elements: [] };
return makeJsxCallNode(tagName, props, emptyChildren);
}

function transformJsxAttributesNative(node: TreeSitterNode): ObjectNode {
const properties: { key: string; value: Expression }[] = [];
const childCount = node.childCount;

for (let i = 0; i < childCount; i++) {
const child = getChild(node, i);
if (!child) continue;
const childBase = child as NodeBase;
if (childBase.type !== "jsx_attribute") continue;

// First named child is property_identifier (key), second is value
const keyNode = getNamedChild(child, 0);
if (!keyNode) continue;
const key = (keyNode as NodeBase).text;

const valueNode = getNamedChild(child, 1);
let value: Expression;

if (!valueNode || (valueNode as NodeBase).isNull) {
// Boolean shorthand: <Input disabled /> → { disabled: true }
value = { type: "boolean", value: true };
} else {
const valueBase = valueNode as NodeBase;
if (valueBase.type === "string") {
value = transformStringNode(valueNode);
} else if (valueBase.type === "jsx_expression") {
const inner = getNamedChild(valueNode, 0);
value = inner ? transformExpression(inner) : { type: "variable", name: "undefined" };
} else {
value = transformExpression(valueNode);
}
}

properties.push({ key, value });
}

return { type: "object", properties };
}

function transformJsxChildrenNative(node: TreeSitterNode): ArrayNode {
const elements: Expression[] = [];
const childCount = node.namedChildCount;

for (let i = 0; i < childCount; i++) {
const child = getNamedChild(node, i);
if (!child) continue;
const childBase = child as NodeBase;

// Skip the open_tag and close_tag — only process content children
if (childBase.type === "jsx_opening_element" || childBase.type === "jsx_closing_element") {
continue;
}

if (childBase.type === "jsx_text") {
const trimmed = childBase.text.trim();
if (trimmed.length === 0) continue;
elements.push({ type: "string", value: trimmed });
} else if (childBase.type === "jsx_expression") {
const inner = getNamedChild(child, 0);
if (inner) {
elements.push(transformExpression(inner));
}
} else if (childBase.type === "jsx_element") {
elements.push(transformJsxElementNative(child));
} else if (childBase.type === "jsx_self_closing_element") {
elements.push(transformJsxSelfClosingElementNative(child));
}
}

return { type: "array", elements };
}

function transformTypeAssertion(node: TreeSitterNode): TypeAssertionNode {
const exprChild = getNamedChild(node, 0);
const expression = exprChild
Expand Down
Loading
Loading