Transforming HTML to JSX
restache extends HTML with curly braces, letting you write React components in HTML that compile to JSX.
Overview
The project aims to offer a simpler alternative to JSX with built-in support for React hooks planned in the roadmap.
Status
🚨 This is early-alpha. It is intended for Go or React devs who are into DSLs and code generation, people building UI builders, or anyone who wants to cut down on JSX boilerplate and help shape the project through feedback.
Example: Dashboard app
The tetsuo/dashboard
repository includes an example setup with ESBuild and a small set of components built with Restache that demonstrate the current state of the art.
👉 See it online – the design's actually pretty sleek.
Try it yourself
Here's an example
Output:
Restache v0 draft
Restache extends HTML5 with Mustache-like syntax to support variables, conditionals, and loops.
Variables
Variables provide access to data in the current scope using dot notation.
Accessing component props
Output:
They can only appear within text nodes or as full attribute values inside a tag.
✅ can insert variable {here}, or <img href={here}>.
When
Renders block when expression is truthy
Output:
Unless
Renders block when expression is falsy
Output:
Range
Iterates over a list value
Output:
Range blocks create a new lexical scope. Inside the block, {name}
refers to the local object in context; outer scope variables are not accessible.
⚠️ Control structures must wrap well-formed elements (or other well-formed control constructs), and cannot appear inside tags.
Comments
There are two types of comments
Output:
Standard HTML comments are removed from the generated output.
Restache comments compile into JSX comments.
Generating JSX
Restache transpiler generates a React JSX component from each input and handles JSX-specific quirks where necessary.
Fragment wrapping
Multiple root elements are wrapped in a Fragment
Output:
- This also applies within control blocks.
- If you only have one root element, then a Fragment is omitted.
Case conversion
React requires component names to start with a capital letter and prop names to use camelCase. In contrast, HTML tags and attribute names are not case-sensitive.
To ensure compatibility, Restache applies the following transformations:
- Elements written in kebab-case (e.g.
<my-button>
) are automatically converted to PascalCase (MyButton
) in the output. - Similarly, kebab-case attributes (like
disable-padding
) are converted to camelCase (disablePadding
).
kebab-case 🔜 React case
Output:
ℹ️ Attributes starting with data-
or aria-
are preserved as-is, in line with React's conventions.
Attribute name normalization
Certain attributes are automatically renamed for React compatibility
Output:
Attribute renaming only occurs when the attribute is valid for the tag. For instance, formaction
isn't renamed on <img>
since it isn't valid there.
However, some attributes are renamed globally, regardless of which element they're used on. These include:
- All standard event handler attributes (
onclick
,onchange
, etc.), which are converted to their camelCased React equivalents (e.g.onClick
,onChange
) - Common HTML aliases and reserved keywords like
class
andfor
, which are renamed toclassName
andhtmlFor
- Certain accessibility- and editing-related attributes, such as
spellcheck
andtabindex
See table.go
for the full list.
Implicit key insertion in loops
When rendering lists, Restache inserts a key
prop automatically, assigning it to the top-level element or to a wrapping Fragment if there are multiple root elements.
Key is passed to the root element inside a loop
Output:
If there are multiple roots, it goes on the Fragment
Output:
Manually set key when there's single root
Output:
Importing other components
Restache supports an implicit module system where custom elements (i.e., tags that are not part of the HTML spec) are automatically resolved to file-based components without requiring explicit imports.
Component imports are inferred from the tag names. The following examples show how different components are resolved:
HTML | JSX | Import path |
---|---|---|
<my-button> |
<MyButton> |
./MyButton |
<ui:card-header> |
<UiCardHeader> |
./ui/CardHeader |
<main> |
<main> |
Not resolved, standard tag |
<ui:div> |
<UiDiv> |
./ui/div |
Component resolution
Any tag that isn't a known HTML element is treated as a component.
When the parser encounters such a tag, it follows these steps:
1. Check for namespace
Restache first determines whether the tag uses a namespace. Namespaced tags contain a prefix and a component name (e.g., <ui:button>
).
2. Standard custom tags
If the tag does not contain a namespace (e.g., <my-button>
):
- Restache first looks for an exact match in the build configuration's
tagMappings
. - If no mapping is found, it falls back to searching in the current directory.
For example,
<my-button>
could resolve to either./my-button
or./MyButton
.
3. Namespaced tags
If the tag does contain a namespace (e.g., <ui:button>
):
Restache first checks the
tagPrefixes
configuration. If a prefix (e.g.,ui
) is defined, it uses the mapped path. For example, ifmui
is mapped to@mui/material
, then<mui:app-bar>
resolves to@mui/material/AppBar
.If no mapping is found, it attempts to resolve the component from a subdirectory: e.g.,
<ui:button>
→ui/Button.js
,ui/button.jsx
,ui/button.tsx
, etc.
ℹ️ Note: Standard HTML tags are not resolved as components, even if identically named files exist in the current directory.
However, namespacing can override this behavior. For example, <ui:div>
will resolve to ./ui/div
(or ./ui/Div
), even though <div>
is a native HTML element.
ESBuild integration
Restache includes an ESBuild plugin that makes integration simple and easy in Go:
- Register
.html
loader and pass it to the plugin - Plugin uses Restache compiler to convert to
.jsx
- No runtime library needed; everything is transpiled ahead of time
The
dashboard
project includes a working build script (build.go
).
There's currently no support for Node.JS environment, but planned.
Roadmap
Integration with React hooks
Currently, most logic must live in a .jsx
file next to the corresponding .html
file.
The long-term plan is to introduce a minimal set of expressions into the language itself so that common hooks like useState
, useContext
, and useSelector
from Redux can be inferred from markup and compiled automatically.
Relational and logical expressions
Support for predicates inside range blocks is planned for v1.
{#products: price < 100 && inStock}
<product-card />
{/products}
This could be compiled as a filter, and potentially mapped to things like backend queries (e.g. MongoDB, CouchDB, Algolia, ...) as well.
Smarter code generation
Before adding expressions, there's still a lot that can be optimized with the syntax that's already in place.
Consider pattern matching. Since Restache doesn't support expressions beyond dot notation, patterns have to be represented structurally. For example, using an object with a mutually exclusive key set:
{
home: {...},
settings: undefined,
products: undefined
}
Then in the template:
{?home} <home /> {/home}
{?settings}<settings />{/settings}
{?products}<products />{/products}
Compiles to:
if (props.home) <Home />
if (props.settings) <Settings />
This results in an O(n)
operation instead of an O(1)
equality check like switch(route)
, but the difference is negligible unless you're dealing with many conditions.
Future versions of Restache will generate if/else
or switch
statements when keyed unions are used, along with other optimizations such as merging adjacent {?x}
and {^x}
blocks into a single conditional.
More codegen targets
Plans include:
- Emitting a real JavaScript AST instead of raw JSX strings
- Supporting things other than React
- Option to emit TSX
When TypeScript is used and type information is available at build time, more advanced optimizations may be possible. But even without that, structural inference allows detecting optionals and iterables, which can be used to emit a generic type for the component.