Define UI components once, as typed data — and render them to vanilla HTML/CSS/JS or to React, Vue, and Svelte components, with styling methodologies (BEM, Tailwind) applied as pluggable extensions.
// button.ts
import { defineTemplate } from '@js-template-engine/types';
export default defineTemplate({
type: 'component',
name: 'Button',
props: {
label: { type: 'string', required: true },
variant: { type: "'primary' | 'secondary'", default: 'primary' },
},
children: [
{
type: 'element',
tag: 'button',
attributes: { class: ['button'], type: 'button' },
conditionalAttributes: [
{
condition: "variant === 'primary'",
attributes: { class: ['button--primary'] },
},
],
events: [{ name: 'click', handler: 'handleClick' }],
children: [{ type: 'text', expression: 'label' }],
},
],
});npx js-template-engine render button.ts # → Button.html
npx js-template-engine render button.ts --framework react # → Button.tsx
npx js-template-engine render button.ts --framework vue # → Button.vue
npx js-template-engine render button.ts --framework svelte # → Button.svelteThe React output, for a taste:
interface ButtonProps {
label: string;
variant?: 'primary' | 'secondary';
}
export function Button(props: ButtonProps) {
const { label, variant = 'primary' } = props;
return (
<button
className={'button' + (variant === 'primary' ? ' button--primary' : '')}
type="button"
onClick={handleClick}
>
{label}
</button>
);
}Tools like Mitosis occupy the "write once, compile everywhere" space by compiling a restricted JSX dialect. This engine takes the opposite approach:
| Mitosis | js-template-engine | |
|---|---|---|
| Authoring surface | Code (a static JSX subset) | Data (typed TS objects / JSON) |
| Intermediate format | Internal, not an authoring format | The data is the format |
| Styling | Out of scope | First-class: BEM, Tailwind, output strategies |
| Default output | Framework targets | Vanilla HTML/CSS/JS, zero extensions required |
Because templates are plain serializable data, any program can assemble, merge, validate, and transform them — a generator, a CMS, a design tool, or an LLM — far more naturally than it could synthesize JSX source. That is what makes scaffold-ui-kit possible: maintain one source of truth, ship component libraries for every framework.
| Package | Purpose |
|---|---|
js-template-engine |
The CLI: render and validate |
scaffold-ui-kit |
Scaffold and build framework-agnostic UI kits |
@js-template-engine/core |
The engine: validation, HTML/CSS/JS output, extension contract |
@js-template-engine/types |
The template format: TypeScript types + JSON Schema |
@js-template-engine/extension-react |
React function components (.tsx) |
@js-template-engine/extension-vue |
Vue single-file components (.vue) |
@js-template-engine/extension-svelte |
Svelte components (.svelte) |
@js-template-engine/extension-bem |
BEM class contribution (block__element--modifier) |
@js-template-engine/extension-tailwind |
Tailwind utility-class contribution |
HTML-first. process(template) produces working HTML, CSS, and
JavaScript with no extensions involved — a zero-dependency rendering that
doubles as the semantic baseline every framework target follows.
Extensions are passed directly. No registry, no string keys, no magic:
import { process } from '@js-template-engine/core';
import { react } from '@js-template-engine/extension-react';
import { bem } from '@js-template-engine/extension-bem';
const result = process(template, { extensions: [react(), bem()] });
// result.files → [{ path: 'Button.tsx', content: '...' }]The core has zero framework knowledge; framework support lives entirely in extensions, and third-party extensions plug in through the same interface.
Concepts live on nodes. Events, conditions, iterations, slots, and styling are embedded in the template nodes themselves — eight node types cover the whole format. Dynamic values are JavaScript expressions carried as opaque strings; the engine emits them into the target syntax and never evaluates them.
Unified output strategies. Styles and scripts share one vocabulary —
inline, in-file, separate-file — so the same template can produce a
single self-contained file or separate .css/.js artifacts.
# Render a template from the command line:
npx js-template-engine render button.ts --framework react --styling bem
# Or scaffold an entire multi-framework component library:
npx scaffold-ui-kit my-ui-kitThe getting-started guide walks through the template format — props, expressions, conditionals, iteration, slots, events, styles — and every render target.
MIT