Skip to content

SchildW3rk/liteforge

Repository files navigation

LiteForge

A signals-based frontend framework with no virtual DOM, zero external dependencies, and TypeScript-first APIs.

MIT License

Why LiteForge?

  • No Virtual DOM — Direct, surgical DOM updates via Signals and Effects
  • Zero-Flicker Architecture — Components render only when async data is fully loaded
  • Fine-Grained Reactivity — Automatic dependency tracking, no manual subscriptions
  • JSX Syntax — Familiar developer experience with build-time optimization
  • Plugin System — First-class AppBuilder.use() API for router, modals, queries and more
  • Type-Safe by Default — Full TypeScript, strict mode, no any in public APIs
  • Zero External Dependencies — Every package has zero runtime deps

Packages

Package Version Size (gzip) Tests Description
liteforge 0.7.0 ~18kb Umbrella package — re-exports @liteforge/core + @liteforge/runtime
@liteforge/core 0.1.0 ~6kb 89 Reactive primitives: signal, computed, effect, batch
@liteforge/runtime 0.6.2 ~12kb 285 Components, lifecycle, control flow, plugin system
@liteforge/store 0.1.0 ~5kb 128 State management with registry and time-travel
@liteforge/router 0.8.0 ~20kb 424 Routing with guards, lazy loading, typed routes, view transitions
@liteforge/query 3.0.0 ~5kb 78 Data fetching with caching and mutations
@liteforge/form 0.1.0 ~4kb 54 Form management with Zod validation
@liteforge/table 0.2.0 ~8kb 72 Data tables with sorting, filtering, pagination
@liteforge/calendar 0.3.0 ~22kb 202 Scheduling calendar with drag & drop and 4 views
@liteforge/client 3.0.0 ~8kb 76 TypeScript-first HTTP client with interceptors and CRUD resources
@liteforge/modal 3.1.0 ~4kb 30 Modal system with focus trap, transitions, and promise presets
@liteforge/toast 2.0.0 ~3kb 39 Imperative toast notifications with four variants
@liteforge/tooltip 0.2.1 ~2kb 28 Portal-based tooltips with auto-positioning and delay
@liteforge/i18n 2.1.0 ~3kb 53 Signals-based i18n with lazy locales, interpolation, pluralization, typed keys
@liteforge/vite-plugin 0.4.3 ~15kb 350 JSX transform and build optimization
@liteforge/devtools 3.0.0 ~16kb 51 Debug panel with 5 tabs and time-travel

1,959 tests across all packages.

Architecture

liteforge         — umbrella (re-exports core + runtime)
│
core  (no deps)
├── runtime       — components, JSX, control flow, plugin system
│
plugins (each installed via .use()):
├── store         — global state
├── router        — client-side routing
├── query         — data fetching
├── form          — form management
├── table         — data tables
├── calendar      — scheduling calendar
├── client        — HTTP client
├── modal         — modal system
├── toast         — toast notifications
├── tooltip       — tooltip system
└── i18n          — internationalization

vite-plugin       — standalone build transform
devtools          — depends on core + store

Quick Start

npm install liteforge @liteforge/vite-plugin

vite.config.ts

import { defineConfig } from 'vite'
import liteforge from '@liteforge/vite-plugin'

export default defineConfig({
  plugins: [liteforge()]
})

main.tsx

import { createApp } from 'liteforge';
import { routerPlugin } from '@liteforge/router';
import { App } from './App.js';

await createApp({ root: App, target: '#app' })
  .use(routerPlugin({ routes: [...] }))
  .mount();

Core Concepts

Signals

import { signal, computed, effect } from 'liteforge'

const count = signal(0)
const doubled = computed(() => count() * 2)

effect(() => {
  console.log(`Count: ${count()}, doubled: ${doubled()}`)
})

count.set(5)           // → "Count: 5, doubled: 10"
count.update(n => n + 1)

Components

import { createComponent } from 'liteforge'

const UserProfile = createComponent({
  async load({ props }) {
    const user = await fetch(`/api/users/${props.userId}`).then(r => r.json())
    return { user }
  },

  placeholder: () => <div class="skeleton" />,

  component: ({ data }) => (
    <div>
      <h1>{data.user.name}</h1>
      <p>{data.user.email}</p>
    </div>
  ),
})

Plugin System

Plugins are installed via AppBuilder.use() before mounting. Each plugin provides typed services that are accessible via use() inside components.

import { createApp } from 'liteforge';
import { routerPlugin } from '@liteforge/router';
import { modalPlugin } from '@liteforge/modal';
import { queryPlugin } from '@liteforge/query';
import { i18nPlugin } from '@liteforge/i18n';
import { devtoolsPlugin } from '@liteforge/devtools';

await createApp({ root: App, target: '#app' })
  .use(routerPlugin({ routes: [...] }))
  .use(modalPlugin())
  .use(queryPlugin())
  .use(i18nPlugin({ defaultLocale: 'en', load: (locale) => import(`./locales/${locale}.js`) }))
  .use(devtoolsPlugin())
  .mount();

Inside components, access plugins via the typed use() function:

const MyPage = createComponent({
  component({ use }) {
    const router = use('router')   // typed as Router
    const modal  = use('modal')    // typed as ModalApi

    return (
      <button onclick={() => router.push('/home')}>Go home</button>
    )
  }
})

Router

import { routerPlugin } from '@liteforge/router'

.use(routerPlugin({
  history: 'browser',   // 'browser' | 'hash' | 'memory'
  scrollBehavior: 'top',
  routes: [
    { path: '/', component: Home },
    { path: '/users/:id', component: () => import('./UserDetail.js') },
    {
      path: '/admin',
      component: AdminLayout,
      guard: 'auth',
      children: [{ path: '/', component: Dashboard }],
    },
  ],
}))

Store

import { defineStore } from '@liteforge/store'

const userStore = defineStore('users', {
  state: { currentUser: null, list: [] },
  getters: (state) => ({
    isLoggedIn: () => state.currentUser() !== null,
  }),
  actions: (state) => ({
    async fetchUsers() {
      state.list.set(await fetch('/api/users').then(r => r.json()))
    },
  }),
})

Query

import { createQuery, createMutation } from '@liteforge/query'

const users = createQuery({
  key: 'users',
  fn: () => fetch('/api/users').then(r => r.json()),
  staleTime: 5 * 60 * 1000,
})

users.data()       // Signal<User[]>
users.isLoading()  // Signal<boolean>
users.refetch()

const addUser = createMutation({
  fn: (data) => api.createUser(data),
  invalidate: ['users'],
})

HTTP Client

import { createClient } from '@liteforge/client'

const client = createClient({ baseUrl: 'https://api.example.com' })

// Low-level
const todo = await client.get<Todo>('/todos/1')

// Resource-based CRUD
const posts = client.resource<Post>('posts')
await posts.getList({ page: 1, pageSize: 20 })
await posts.getOne(42)
await posts.create({ title: 'Hello', body: '...' })
await posts.update(42, { title: 'Updated' })
await posts.delete(42)

// Interceptors
client.addInterceptor({
  onRequest: (config) => ({ ...config, headers: { ...config.headers, Authorization: `Bearer ${token}` } }),
  onResponseError: (error) => { if (error.status === 401) redirect('/login'); throw error; },
})

Modal

import { createModal, confirm, alert } from '@liteforge/modal'

// Declarative
const dialog = createModal({
  title: 'Edit User',
  content: () => <EditUserForm />,
})
dialog.open()

// Promise presets
const confirmed = await confirm({ title: 'Delete?', message: 'This cannot be undone.' })
await alert({ title: 'Done', message: 'User deleted.' })

Toast

import { toast } from '@liteforge/toast'

toast.success('Saved successfully')
toast.error('Something went wrong')
toast.warning('Unsaved changes')
toast.info('New version available')

// With options
toast.success('User created', { duration: 4000, position: 'bottom-right' })

Tooltip

import { tooltip } from '@liteforge/tooltip'

// Imperative — attach to any element
const cleanup = tooltip(buttonEl, {
  content: 'Save changes',
  position: 'top',        // 'top' | 'bottom' | 'left' | 'right'
  delay: 300,             // ms before showing
  showWhen: () => !isMobile(),  // reactive guard
})

// Declarative — JSX component
import { Tooltip } from '@liteforge/tooltip'

<Tooltip content="Save changes" position="top">
  <button>Save</button>
</Tooltip>

Internationalization (i18n)

npm install @liteforge/i18n
// main.tsx
import { i18nPlugin } from '@liteforge/i18n';

await createApp({ root: App, target: '#app' })
  .use(i18nPlugin({
    defaultLocale: 'en',
    fallbackLocale: 'en',          // used for missing keys
    load: async (locale) => {
      const mod = await import(`./locales/${locale}.js`);
      return mod.default;          // plain TranslationTree object
    },
    persist: true,                 // saves locale to localStorage
    storageKey: 'my-locale',       // default: 'lf-locale'
  }))
  .mount();
// locales/en.ts
export default {
  greeting: 'Hello, {name}!',
  nav: { home: 'Home', settings: 'Settings' },
  items: '{count} item | {count} items',           // 2-part: singular | plural
  messages: 'No messages | {count} message | {count} messages', // 3-part: zero | one | many
} satisfies TranslationTree;
// Inside a component
const MyPage = createComponent({
  component({ use }) {
    const { t, locale, setLocale } = use('i18n');

    return (
      <div>
        <p>{() => t('greeting', { name: 'World' })}</p>
        <p>{() => t('items', { count: count() }, count())}</p>
        <p>{() => t('nav.home')}</p>

        <button onclick={() => setLocale('de')}>🇩🇪 Deutsch</button>
        <button onclick={() => setLocale('en')}>🇬🇧 English</button>
      </div>
    );
  },
});

Key properties:

  • No re-render — only the text nodes that call t() update on locale switch
  • Fallback locale — missing keys in the current locale transparently fall back
  • Async plugin install — initial locale is fully loaded before the app mounts (no flash of untranslated keys)
  • Dot-notation keyst('nav.home') traverses nested objects
  • Pipe pluralization2-part (singular|plural) or 3-part (zero|one|many)

Control Flow

import { Show, For, Switch, Match } from 'liteforge'

<Show when={() => user()}>
  <UserCard user={user} />
</Show>

<For each={() => items()}>
  {(item) => <li>{item.name}</li>}
</For>

<Switch fallback={<NotFound />}>
  <Match when={() => status() === 'loading'}>Loading...</Match>
  <Match when={() => status() === 'error'}>Error!</Match>
</Switch>

Development

# Install dependencies
pnpm install

# Build all packages
pnpm build:packages

# Run all tests (1,796 tests)
pnpm test

# Type check all packages
pnpm typecheck:all

# Run starter demo app
pnpm --filter starter dev

# Run docs app
pnpm --filter docs dev

Bundle Sizes

Setup Size (gzip)
Minimal (core only) ~6kb
Core + Runtime ~18kb
Core + Runtime + Store + Router ~43kb
Full stack (+ query + client + modal + toast + tooltip) ~65kb

Status

LiteForge is in active development. APIs may change between minor versions. I use it in my own production projects, but if you adopt it today, expect some rough edges.

LiteForge is a personal framework born from real frustration with React's re-rendering model and Vue's adapter overhead. I built it because I wanted a tool that works the way I think — signals that directly update the DOM, no virtual DOM diffing, no magic.

I'm actively building real applications on top of it (scheduling software, business tools, DevOps dashboards), so the framework evolves based on actual needs, not theoretical purity.

What works well today: Core reactivity, routing, state management, forms, data tables, calendar, HTTP client, modals, i18n, and a full plugin system — all battle-tested through my own projects.

What's still maturing: Documentation, edge cases in complex layouts, and the ecosystem around it.

If you find it useful, feel free to use it. If you find a bug, I'd appreciate an issue. PRs are welcome but please open an issue first.

Built with AI

I want to be transparent: LiteForge was developed with significant AI assistance. I used Claude (Anthropic) as a development partner throughout the entire process — from architecture decisions to implementation, testing, and documentation.

What that means in practice:

  • I designed the API, made all architecture decisions, and defined what the framework should do
  • AI helped write implementation code, tests, and documentation based on my specifications
  • Every feature was reviewed, tested, and validated by me in real browser environments
  • The framework reflects my opinions and preferences as a developer, not generic AI output

I believe AI-assisted development is the future of how software gets built. Being upfront about it is more honest than pretending otherwise. The code quality speaks for itself — 1,796 tests, TypeScript strict mode, zero external dependencies.

About

LiteForge is built and maintained by SchildW3rk — a one-person software studio from Salzburg, Austria.

License

MIT — see LICENSE for details.

About

A lightweight, signals-based frontend framework. No virtual DOM, fine-grained reactivity, JSX syntax. ~6kb core.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors