Gracile — JSX Forge (Vite plugin)
A JSX Forge convenience wrapper for Vite
projects.
Compiles JSX/TSX to html tagged template literals at build time, using
TypeScript & ts-patch.
Tip
You can use this plugin with any Vite+Lit setup!
It’s totally decoupled from the Gracile framework.
Installation
npm i @gracile-labs/vite-plugin-jsx-forge jsx-forge
Setup
Vite config
📄 /vite.config.ts
import { gracileJsxToLiteralsimport gracileJsxToLiterals } from '@gracile-labs/vite-plugin-jsx-forge/to-literals';
import { defineConfigfunction defineConfig(config: UserConfig): UserConfig (+5 overloads)Type helper to make it easier to use vite.config.ts
accepts a direct
{@link
UserConfig
}
object, or a function that returns it.
The function receives a
{@link
ConfigEnv
}
object.
} from 'vite';
export default defineConfigfunction defineConfig(config: UserConfig): UserConfig (+5 overloads)Type helper to make it easier to use vite.config.ts
accepts a direct
{@link
UserConfig
}
object, or a function that returns it.
The function receives a
{@link
ConfigEnv
}
object.
({
pluginsUserConfig.plugins?: PluginOption[] | undefinedArray of vite plugins to use.
: [
gracileJsxToLiteralsimport gracileJsxToLiterals (),
// ...
],
});
TypeScript config
ts-patch must be installed and prepared for the compiler plugin to work.
📄 /tsconfig.json
{
"compilerOptions": {
"jsx": "preserve",
"jsxImportSource": "jsx-forge",
"plugins": [
{
"transform": "jsx-forge/transform",
},
],
// Should be aligned with Rollup output directory.
"outDir": "dist",
},
}
Usage
JSX is compiled statically to Lit html tagged templates. This is not a JSX
runtime — think Solid-style compilation, but targeting Lit (or any
compatible tagged template library).
Basic example
📄 /src/my-template.tsx
const nameconst name: "World" = 'World';
const elconst el: any = <span classclass: string ="greeting">{nameconst name: "World" }</span>;
Compiles to:
import { htmlconst html: (strings: TemplateStringsArray, ...values: unknown[]) => TemplateResult<1>Interprets a template literal as an HTML template that can efficiently
render to and update a container.
const header = (title: string) => html`<h1>${title}</h1>`;
The html tag returns a description of the DOM to render as a value. It is
lazy, meaning no work is done until the template is rendered. When rendering,
if a template comes from the same expression as a previously rendered result,
it's efficiently updated instead of replaced.
} from 'lit';
const nameconst name: "World" = 'World';
const elconst el: TemplateResult<1> = htmlconst html: (strings: TemplateStringsArray, ...values: unknown[]) => TemplateResult<1>Interprets a template literal as an HTML template that can efficiently
render to and update a container.
const header = (title: string) => html`<h1>${title}</h1>`;
The html tag returns a description of the DOM to render as a value. It is
lazy, meaning no work is done until the template is rendered. When rendering,
if a template comes from the same expression as a previously rendered result,
it's efficiently updated instead of replaced.
`<span class="greeting">${nameconst name: "World" }</span>`;
Custom element rendering
📄 /src/features/my-element.el.tsx
'use html-signal';
import { customElementconst customElement: (tagName: string) => CustomElementDecoratorClass decorator factory that defines the decorated class as a custom element.
} from 'lit/decorators/custom-element.js';
import { LitElementclass LitElementBase element class that manages element properties and attributes, and
renders a lit-html template.
To define a component, subclass LitElement and implement a
render method to provide the component's template. Define properties
using the
{@linkcode
LitElement.properties
properties
}
property or the
{@linkcode
property
}
decorator.
} from 'lit';
@customElementfunction customElement(tagName: string): CustomElementDecoratorClass decorator factory that defines the decorated class as a custom element.
('my-element')
export class MyElementclass MyElement extends LitElementclass LitElementBase element class that manages element properties and attributes, and
renders a lit-html template.
To define a component, subclass LitElement and implement a
render method to provide the component's template. Define properties
using the
{@linkcode
LitElement.properties
properties
}
property or the
{@linkcode
property
}
decorator.
{
override renderMyElement.render(): anyInvoked on each update to perform rendering tasks. This method may return
any value renderable by lit-html's ChildPart - typically a
TemplateResult. Setting properties inside this method will not trigger
the element to update.
() {
return (
<>
<h1>Hello</h1>
<button onon:click: () => void :clickon:click: () => void ={() => consolevar console: Console .logConsole.log(...data: any[]): voidThe console.log() static method outputs a message to the console.
('clicked!')}>Click me</button>
</>
);
}
}
Server document
📄 /src/document.tsx
'use html-server';
export const documentconst document: (props: {
url: URL;
title?: string;
}) => any
= (propsprops: {
url: URL;
title?: string;
}
: { urlurl: URL : URL; titletitle?: string | undefined ?: string }) => (
<html langlang: string ="en">
<head>
<meta charsetcharset: string ="utf-8" />
<meta namename: string ="viewport" contentcontent: string ="width=device-width, initial-scale=1.0" />
<title>{propsprops: {
url: URL;
title?: string;
}
.titletitle?: string | undefined ?? 'My App'}</title>
</head>
<body>
<route-template-outlet></route-template-outlet>
</body>
</html>
);
Template flavor directives
Place a "use html-*" directive at the top of a file to control which html
tag function is imported:
| Directive | Import source | Use case |
|---|---|---|
| (default) | lit | Standard client templates |
'use html-server' | @lit-labs/ssr | Server-side rendering |
'use html-signal' | @lit-labs/signals | Signal-aware templates |
Options
gracileJsxToLiterals({
/** Use a full TS LanguageService for type-aware transforms. Default: true */
typeAware: true,
/** Path to tsconfig (resolved from project root). Default: 'tsconfig.json' */
tsconfig: 'tsconfig.json',
});
typeAware (default: true)
When enabled, the plugin maintains a TypeScript LanguageService that provides a type checker. This enables:
- Automatic boolean bindings —
disabled={flag}→?disabled=${flag}whenflagis typedboolean. - Automatic
ifDefined()wrapping —title={val}→title=${ifDefined(val)}whenvalis typedT | undefined. - Spread attribute expansion —
{...props}is expanded into individual attribute bindings using the type checker.
When set to false, the transform is purely syntactic (no type checker).
This is significantly faster (~20× on incremental saves) but requires you to use
explicit namespace prefixes for special bindings:
| Prefix | Lit binding | Example |
|---|---|---|
bool: | ?attr | <input bool:checked={v}/> |
if: | ifDefined() | <a if:href={maybeUrl}> |
on: | @event | <button on:click={fn}> |
.prop: | .property | <el .prop:items={list}> |
Spread attributes are skipped in syntactic mode (a console warning is emitted).
Benchmarks
| Scenario | median | mean | p95 | min | max |
|---|---|---|---|---|---|
| Cold start .tsx (type-aware) | — | 1186.6 ms | — | — | — |
| Cold start .tsx (syntactic) | — | 34.9 ms | — | — | — |
| Cold start .ts | — | 11.3 ms | — | — | — |
| Warm request .tsx (type-aware) | 2.0 ms | 2.1 ms | 3.3 ms | 1.4 ms | 3.3 ms |
| Warm request .tsx (syntactic) | 1.7 ms | 1.7 ms | 2.8 ms | 1.2 ms | 2.8 ms |
| Warm request .ts (baseline) | 2.9 ms | 3.4 ms | 12.0 ms | 2.2 ms | 12.0 ms |
| Incremental .tsx (type-aware) | 95.9 ms | 103.3 ms | 153.4 ms | 89.1 ms | 153.4 ms |
| Incremental .tsx (syntactic) | 5.8 ms | 6.1 ms | 8.7 ms | 5.0 ms | 8.7 ms |
| Incremental .ts (baseline) | 4.5 ms | 4.7 ms | 5.7 ms | 4.0 ms | 5.7 ms |
See the full JSX Forge documentation for the complete syntax reference (bindings, components, control helpers, etc.).