Skip to content

About

Compile components written in a Svelte 5-style DSL to React, Vue, SolidJS, Svelte, or Web Components.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Durable Component Compiler

Write once, compile anywhere. Eliminate framework lock-in forever.

Tests License Status

A production-ready component compiler that lets you write components once in a clear, Svelte 5-inspired DSL and compile them to any major framework: React, Vue, SolidJS, Svelte, or Web Components.

πŸš€ Quick Start

1. Write a component in .dce format:

<script>
  let { initialCount = 0 } = $props();
  let count = $state(initialCount);
  let doubled = $derived(count * 2);

  function increment() {
    count++;
  }
</script>

<template>
  <div class="counter">
    <button on:click={increment}>
      Count: {count} | Doubled: {doubled}
    </button>
  </div>
</template>

<style>
  .counter { padding: 2rem; }
  button {
    background: linear-gradient(135deg, #667eea, #764ba2);
    color: white;
  }
</style>

2. Compile to your target framework:

# Compile to React
npx dcc compile Counter.dce --target react -o Counter.jsx

# Compile to SolidJS
npx dcc compile Counter.dce --target solid -o Counter.jsx

# Compile to Svelte 5
npx dcc compile Counter.dce --target svelte -o Counter.svelte

3. Use in your app:

// React
import { Counter } from './Counter';
import './Counter.css';
<Counter initialCount={5} />
<!-- Svelte 5 -->
<script>
  import Counter from './Counter.svelte';
</script>
<Counter initialCount={5} />

✨ Why Durable Components?

The Problem

Framework churn creates massive technical debt:

  • Component libraries locked to aging frameworks
  • Expensive, risky rewrites every few years
  • Shrinking talent pool for legacy frameworks
  • Cost of waiting compounds annually

The Solution

Write once, compile to any framework. Your components become portable assets that can:

  • βœ… Target React today, Vue tomorrow, without rewrites
  • βœ… Generate both native framework code AND standard Web Components
  • βœ… Survive framework churn with zero migration cost
  • βœ… Integrate seamlessly with existing codebases

Built on Durable Programming Principles for long-term stability.

🎯 Features

βœ… Current (Phase 1-2 Complete)

  • React Compilation - Full support with Hooks (useState, useMemo, useEffect)
  • SolidJS Compilation - Native Signals output (createSignal, createMemo, createEffect)
  • Svelte 5 Compilation - Native Runes output (reverse transformation)
  • Scoped CSS - Svelte/Vue-style scoped styles with attribute selectors
  • Explicit Reactivity - Svelte 5 Runes ($state, $props, $derived, $effect)
  • Vue 3 Compilation - Composition API output (<script setup>, ref, computed)
  • Template Directives - {#if}, {#each}, on:event, bind:value
  • Behavior Primitives - Focus trap, Escape, scroll lock, and auto-dismiss timers, portable across every target
  • Component Flattening - Automatic recursive compilation of DCE component dependencies
  • CLI Tool - Simple dcc command for compilation
  • Programmatic API - compile() function for build tools
  • Vite Plugin - Seamless integration with Vite build tool
  • TypeScript Support - Full type definitions included
  • Comprehensive Test Coverage - Generated output is verified against each framework's real compiler

🚧 Roadmap (Phase 3+)

  • Web Components (standards-based)
  • UnoCSS integration
  • Webpack plugin
  • VS Code extension

πŸ“š Documentation

πŸ—οΈ Architecture

.dce Source β†’ Parser β†’ D-AST β†’ Transformer β†’ JSON IR β†’ Generator β†’ React/Vue/etc.

Three-Phase Design

  1. Parser - Converts .dce files into a Durable AST (D-AST)
  2. Transformer - Creates a framework-agnostic JSON Intermediate Representation (IR)
  3. Generator - Produces target-specific code from IR

The JSON IR is the "source of truth" - a canonical representation that maps 1:1 to all major frameworks' reactive primitives.

πŸŽ“ DSL Syntax

Based on Svelte 5 Runes for explicit, portable reactivity:

// Reactive state
let count = $state(0);

// Props with defaults
let { name = 'World' } = $props();

// Computed values
let doubled = $derived(count * 2);

// Side effects
$effect(() => {
  console.log('Count changed:', count);
});

// Event handlers (plain functions)
function increment() {
  count++;
}

Behavior Primitives

Interaction behaviors that every framework needs but none spells the same way. Each compiles to a mount/unmount effect β€” useEffect on React, onMount/onCleanup on Solid, onMounted/onUnmounted on Vue, $effect on Svelte β€” with the teardown wired in, so nothing leaks when the component unmounts.

Primitive Purpose
<dce:focus-trap for={el} /> Confine Tab navigation to an element; restores focus on teardown
<dce:escape on:escape={fn} /> Run a handler on the Escape key
<dce:scroll-lock /> Prevent the page behind from scrolling; nested locks are reference-counted
<dce:timer after={ms} on:elapsed={fn} /> Auto-dismiss timer; a non-positive delay disables it
<script>
  let { onClose } = $props();
  let dialog;
</script>

<template>
  <div class="overlay">
    <dce:scroll-lock />
    <dce:escape on:escape={onClose} />
    <dce:timer after={5000} on:elapsed={onClose} />
    <dce:focus-trap for={dialog} />

    <div bind:this={dialog} role="dialog">
      <slot />
    </div>
  </div>
</template>

The helper implementations are emitted inline alongside the component, so generated output stays standalone with no runtime dependency. Only the helpers a component actually uses are included.

The focus trap honours [autofocus], skips hidden and disabled elements, and cycles in both directions. The scroll lock compensates for scrollbar width so the page does not shift when it engages.

Inside {#each}

A primitive that reads a loop binding belongs to one item, not to the component, so it compiles per item rather than to a single hoisted effect:

{#each toasts as toast}
  <div class="toast">{toast.text}</div>
  <dce:timer after={toast.ms} on:elapsed={() => dismiss(toast.id)} />
{/each}

Each item gets its own timer, and that timer is torn down when the item leaves the list β€” not only when the whole component unmounts. React and Solid emit a small per-item component, Svelte an action, and Vue a custom directive.

A primitive that reads nothing from the loop, such as a bare <dce:scroll-lock />, still compiles to one component-level effect, which is both correct and cheaper than one per item.

Element References

bind:this={panel} declares panel and fills it with the element. Script code uses the bare name, and each target's accessor is added on the way out β€” panel.current on React, panel.value on Vue, the variable itself on Solid and Svelte:

<script>
  let panel;
  function focusIt() { panel.focus(); }
</script>

<template>
  <div bind:this={panel} tabindex="-1"></div>
</template>

Two situations produce a warning on compile()'s warnings array rather than questionable output:

Code Meaning
REF_SHADOWS_STATE A name is both $state and a bind:this target. bind:this declares the reference itself, so the $state declaration is redundant and is dropped.
REF_BOUND_IN_LOOP A bind:this inside an {#each}. One name cannot hold every element the loop renders, and the targets disagree on what survives, so move the element into a child component.

Diagnostics

Warnings do not fail a build. They reach you through whichever entry point you compile with:

  • dcc compile prints them to stderr, so piping the compiled output somewhere keeps that output clean.
  • The Vite plugin reports them through this.warn, so they appear in the dev server output and the build log.
  • The showcase server prints each one once per component and again after you edit the file, rather than on every recompile.
  • compile() returns them on result.warnings for anything building on the API directly.

With includeReferences enabled, a warning from a component your entry point imports is reported too, prefixed with the file it came from.

πŸ“¦ Installation

npm install @durable/compiler

πŸ”¨ Usage

Vite Plugin (Recommended)

The easiest way to use Durable Components in your project is with the Vite plugin:

npm install @durable/compiler
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { durableComponents } from '@durable/compiler/vite-plugin';

export default defineConfig({
  plugins: [
    react(), // or solid(), svelte(), etc.
    durableComponents({
      target: 'react', // 'react' | 'solid' | 'svelte' | 'vue'
      style: 'scoped'  // 'scoped' | 'inline' | 'unocss'
    })
  ]
});

Then just import .dce files directly in your code:

// App.tsx
import Counter from './Counter.dce';

function App() {
  return <Counter initialCount={5} />;
}

Features:

  • βœ… Hot Module Replacement (HMR)
  • βœ… Automatic CSS extraction and injection
  • βœ… TypeScript support
  • βœ… Dev mode with better error messages

See examples/vite-demo for complete examples with React, SolidJS, and Svelte.

CLI

# Compile to React
dcc compile Counter.dce --target react -o Counter.jsx

# Compile to SolidJS
dcc compile Counter.dce --target solid -o Counter.jsx

# Compile to Svelte 5
dcc compile Counter.dce --target svelte -o Counter.svelte

# Show available targets
dcc info

Programmatic API

import { compile } from '@durable/compiler';
import fs from 'fs';

const source = fs.readFileSync('Counter.dce', 'utf-8');

const result = compile(source, {
  filename: 'Counter.dce',
  target: 'react',  // 'react' | 'vue' | 'solid' | 'svelte' | 'wc'
  style: 'scoped'   // 'scoped' | 'inline' | 'unocss'
});

fs.writeFileSync('Counter.jsx', result.js.code);
fs.writeFileSync('Counter.css', result.css?.code || '');

πŸ§ͺ Development

# Install dependencies
npm install

# Build
npm run build

# Run tests
npm test

# Watch mode
npm run dev

# Compile examples
npm run build && node dist/cli/index.js compile examples/Counter.dce --target react

πŸ“Š IR-to-Framework Translation Matrix

DSL Construct React Vue 3 SolidJS Svelte 5
$state(0) useState(0) ref(0) createSignal(0) $state(0)
$derived(x*2) useMemo(()=>x*2,[x]) computed(()=>x*2) createMemo(()=>x*2) $derived(x*2)
$effect(...) useEffect(...,[deps]) watchEffect(...) createEffect(...) $effect(...)
$props() function(props) defineProps() function(props) $props()

🌟 Philosophy

Built on Durable Programming Principles:

  • Technical Flexibility - Pluggable architecture, never bound to one framework
  • Clarity over Cleverness - Explicit reactivity, no magic
  • Pragmatic Approach - Common subset of features that work everywhere
  • Business-Centric - Reduces technical debt, increases component lifespan

🀝 Contributing

Contributions welcome! To add a new generator:

  1. Create src/generators/{target}.ts
  2. Implement IR β†’ Target transformation (see generators/react.ts)
  3. Add tests
  4. Update documentation

πŸ“„ License

MIT


Built with ❀️ by the Durable Programming community

Report Bug Β· Request Feature Β· Documentation

About

Compile components written in a Svelte 5-style DSL to React, Vue, SolidJS, Svelte, or Web Components.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages