Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
6 changes: 6 additions & 0 deletions .changeset/jsx-in-nested-statements.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@astrojs/compiler-binding": patch
"@astrojs/compiler-rs": patch
---

Fixes JSX not being transformed inside function declarations, class declarations and expressions, `throw` statements, and `for`-loop initializers.
215 changes: 201 additions & 14 deletions crates/astro_codegen/src/printer/expressions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,9 +176,11 @@ impl<'a> AstroCodegen<'a> {
}
Expression::FunctionExpression(func) => {
self.add_source_mapping_for_span(expr.span());
// Handle regular/async/generator function expressions that may
// contain JSX (e.g. `async function* () { yield <Foo /> }`)
self.print_function_expression(func);
self.print_function(func);
}
Expression::ClassExpression(class) => {
self.add_source_mapping_for_span(expr.span());
self.print_class(class);
}
Expression::YieldExpression(yield_expr) => {
self.add_source_mapping_for_span(expr.span());
Expand Down Expand Up @@ -549,8 +551,7 @@ impl<'a> AstroCodegen<'a> {
self.add_source_mapping_for_span(for_stmt.span);
self.print("for(");
if let Some(init) = &for_stmt.init {
let code = gen_to_string(init);
self.print(&code);
self.print_for_init(init);
}
self.print(";");
if let Some(test) = &for_stmt.test {
Expand Down Expand Up @@ -608,6 +609,22 @@ impl<'a> AstroCodegen<'a> {
self.print(": ");
self.print_jsx_aware_statement(&labeled.body);
}
Statement::FunctionDeclaration(func) => {
self.add_source_mapping_for_span(func.span);
self.print_function(func);
self.print("\n");
}
Statement::ClassDeclaration(class) => {
self.add_source_mapping_for_span(class.span);
self.print_class(class);
self.print("\n");
}
Statement::ThrowStatement(throw_stmt) => {
self.add_source_mapping_for_span(throw_stmt.span);
self.print("throw ");
self.print_expression(&throw_stmt.argument);
self.print(";\n");
}
_ => {
self.add_source_mapping_for_span(stmt.span());
// For other statements, use regular codegen
Expand All @@ -618,9 +635,8 @@ impl<'a> AstroCodegen<'a> {
}
}

/// Print a regular/async/generator function expression with JSX-aware body.
pub(super) fn print_function_expression(&mut self, func: &oxc_ast::ast::Function<'a>) {
// `async function* name(params) { body }`
/// Print a function declaration or expression with JSX-aware body.
pub(super) fn print_function(&mut self, func: &oxc_ast::ast::Function<'a>) {
if func.r#async {
self.print("async ");
}
Expand All @@ -633,16 +649,51 @@ impl<'a> AstroCodegen<'a> {
self.print(id.name.as_str());
}
self.print("(");
let params = &func.params;
for (i, param) in params.items.iter().enumerate() {
if i > 0 {
self.print_formal_parameters(&func.params);
self.print(") ");
if let Some(body) = &func.body {
self.print_jsx_aware_function_body(body);
}
}

/// Print a `FormalParameters` list, including default-value initializers
/// and the rest parameter. No surrounding parens.
///
/// Parameter-property modifiers and decorators have runtime effect (Phase 2
/// turns them into `this.x = x` assignments), so they're emitted faithfully.
pub(super) fn print_formal_parameters(&mut self, params: &oxc_ast::ast::FormalParameters<'a>) {
use oxc_ast::ast::TSAccessibility;
let mut first = true;
for param in &params.items {
if !first {
self.print(", ");
}
first = false;
self.print_decorators(&param.decorators);
match param.accessibility {
Some(TSAccessibility::Public) => self.print("public "),
Some(TSAccessibility::Protected) => self.print("protected "),
Some(TSAccessibility::Private) => self.print("private "),
None => {}
}
if param.r#override {
self.print("override ");
}
if param.readonly {
self.print("readonly ");
}
self.print_binding_pattern(&param.pattern);
if let Some(init) = &param.initializer {
self.print(" = ");
self.print_expression(init);
}
}
self.print(") ");
if let Some(body) = &func.body {
self.print_jsx_aware_function_body(body);
if let Some(rest) = &params.rest {
if !first {
self.print(", ");
}
self.print("...");
self.print_binding_pattern(&rest.rest.argument);
}
}

Expand All @@ -655,4 +706,140 @@ impl<'a> AstroCodegen<'a> {
self.print(&code);
}
}

/// Print leading decorators as `@expr `.
fn print_decorators(&mut self, decorators: &[oxc_ast::ast::Decorator<'a>]) {
for decorator in decorators {
self.print("@");
self.print_expression(&decorator.expression);
self.print(" ");
}
}

/// Print a class declaration or expression with JSX-aware method bodies,
/// property initializers, and static blocks.
pub(super) fn print_class(&mut self, class: &oxc_ast::ast::Class<'a>) {
self.print_decorators(&class.decorators);
self.print("class");
if let Some(id) = &class.id {
self.print(" ");
self.print(id.name.as_str());
}
if let Some(super_class) = &class.super_class {
self.print(" extends ");
self.print_expression(super_class);
}
self.print(" {\n");
for element in &class.body.body {
self.print_class_element(element);
}
self.print("}");
}

fn print_class_element(&mut self, element: &oxc_ast::ast::ClassElement<'a>) {
use oxc_ast::ast::{ClassElement, MethodDefinitionKind};
match element {
ClassElement::MethodDefinition(method) => {
self.add_source_mapping_for_span(method.span);
self.print_decorators(&method.decorators);
if method.r#static {
self.print("static ");
}
match method.kind {
MethodDefinitionKind::Get => self.print("get "),
MethodDefinitionKind::Set => self.print("set "),
MethodDefinitionKind::Constructor | MethodDefinitionKind::Method => {}
}
if method.value.r#async {
self.print("async ");
}
if method.value.generator {
self.print("*");
}
if method.computed {
self.print("[");
}
let key_code = gen_to_string(&method.key);
self.print(&key_code);
if method.computed {
self.print("]");
}
self.print("(");
self.print_formal_parameters(&method.value.params);
self.print(") ");
if let Some(body) = &method.value.body {
self.print_jsx_aware_function_body(body);
}
self.print("\n");
}
ClassElement::PropertyDefinition(prop) => {
self.add_source_mapping_for_span(prop.span);
self.print_decorators(&prop.decorators);
if prop.r#static {
self.print("static ");
}
if prop.computed {
self.print("[");
}
let key_code = gen_to_string(&prop.key);
self.print(&key_code);
if prop.computed {
self.print("]");
}
if let Some(value) = &prop.value {
self.print(" = ");
self.print_expression(value);
}
self.print(";\n");
}
ClassElement::StaticBlock(block) => {
self.add_source_mapping_for_span(block.span);
self.print("static {\n");
for stmt in &block.body {
self.print_jsx_aware_statement(stmt);
}
self.print("}\n");
}
// TSIndexSignature is type-only; AccessorProperty rarely carries
// JSX. Both fall back to plain codegen.
ClassElement::AccessorProperty(_) | ClassElement::TSIndexSignature(_) => {
let code = gen_to_string(element);
self.print(&code);
self.print("\n");
}
}
}

fn print_for_init(&mut self, init: &oxc_ast::ast::ForStatementInit<'a>) {
match init {
oxc_ast::ast::ForStatementInit::VariableDeclaration(decl) => {
let kind = match decl.kind {
VariableDeclarationKind::Var => "var",
VariableDeclarationKind::Let => "let",
VariableDeclarationKind::Const => "const",
VariableDeclarationKind::Using => "using",
VariableDeclarationKind::AwaitUsing => "await using",
};
for (i, declarator) in decl.declarations.iter().enumerate() {
if i == 0 {
self.print(kind);
self.print(" ");
} else {
self.print(", ");
}
let pattern_code = gen_to_string(&declarator.id);
self.print(&pattern_code);
if let Some(init_expr) = &declarator.init {
self.print(" = ");
self.print_expression(init_expr);
}
}
}
other => {
if let Some(expr) = other.as_expression() {
self.print_expression(expr);
}
}
}
}
}
41 changes: 14 additions & 27 deletions crates/astro_codegen/src/printer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -349,43 +349,30 @@ impl<'a> AstroCodegen<'a> {
self.code.print_char('\n');
}

/// Print arrow function parameters including parentheses and the `=>` arrow.
///
/// Prints the async prefix (if applicable), the parameter list (with
/// optional parentheses for single-identifier params), and ` => `.
/// The caller is responsible for printing the body.
/// Print the arrow function header: async prefix, params, ` => `.
/// The caller prints the body.
fn print_arrow_params(&mut self, arrow: &ArrowFunctionExpression<'a>) {
if arrow.r#async {
self.print("async ");
}
// Single simple identifier param doesn't need parens, but destructuring patterns do
let needs_parens = arrow.params.items.len() != 1
|| arrow.params.rest.is_some()
|| !matches!(
arrow.params.items.first().map(|p| &p.pattern),
Some(BindingPattern::BindingIdentifier(_))
);
|| arrow
.params
.items
.first()
.map(|p| {
!matches!(p.pattern, BindingPattern::BindingIdentifier(_))
|| p.initializer.is_some()
|| p.type_annotation.is_some()
|| p.optional
})
.unwrap_or(true);

if needs_parens {
self.print("(");
}

let mut first = true;
for param in &arrow.params.items {
if !first {
self.print(", ");
}
first = false;
self.print_binding_pattern(&param.pattern);
}
if let Some(rest) = &arrow.params.rest {
if !first {
self.print(", ");
}
self.print("...");
self.print_binding_pattern(&rest.rest.argument);
}

self.print_formal_parameters(&arrow.params);
if needs_parens {
self.print(")");
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
() => {
class Renderer {
render() {
return <Component />;
}
}
return new Renderer().render();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
source: crates/astro_codegen/tests/snapshots.rs
input_file: crates/astro_codegen/tests/fixtures/class_decl_in_arrow_with_jsx.astro
---
import { render as $$render, createComponent as $$createComponent, renderComponent as $$renderComponent, createMetadata as $$createMetadata } from "http://localhost:3000/";
export const $$metadata = $$createMetadata(import.meta.url, {
modules: [],
hydratedComponents: [],
clientOnlyComponents: [],
hydrationDirectives: new Set([]),
hoisted: []
});
const $$Component = $$createComponent(($$result, $$props, $$slots) => {
return $$render`${() => {
class Renderer {
render() {
return $$render`${$$renderComponent($$result, "Component", Component, {})}`;
}
}
return new Renderer().render();
}}`;
}, undefined, undefined);
export default $$Component;
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
() => {
@sealed
class Widget {
@log
render() {
return <Component />;
}
}
return new Widget().render();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
source: crates/astro_codegen/tests/snapshots.rs
assertion_line: 81
input_file: crates/astro_codegen/tests/fixtures/class_decl_with_decorators.astro
---
import { render as $$render, createComponent as $$createComponent, renderComponent as $$renderComponent, createMetadata as $$createMetadata } from "http://localhost:3000/";
export const $$metadata = $$createMetadata(import.meta.url, {
modules: [],
hydratedComponents: [],
clientOnlyComponents: [],
hydrationDirectives: new Set([]),
hoisted: []
});
const $$Component = $$createComponent(($$result, $$props, $$slots) => {
return $$render`${() => {
@sealed class Widget {
@log render() {
return $$render`${$$renderComponent($$result, "Component", Component, {})}`;
}
}
return new Widget().render();
}}`;
}, undefined, undefined);
export default $$Component;
Loading
Loading