An HTML-first, eval-free client-side rendering (CSR) engine built with standard browser APIs. Templates stay in declarative HTML <template> elements; an unprivileged Micro-Kernel orchestrates built-in template composition and discrete extensible modules for conditionals, loops, text, visibility, two-way form bindings, delegated events, and DOM element references.
Zero compilation. Zero virtual DOM. Zero eval or new Function. Strict Content Security Policy (CSP) compatible out of the box.
- HTML-First Templates: Author templates in native
<template>tags. No JSX, no compiler, no build step required during development. - Unprivileged Micro-Kernel: The kernel provides generic triggers, routing, prototypal scope, and lifecycle management without any hardcoded feature semantics.
- Built-in Composition: Core
<partial>and<slot>composition with isolated template scope, caller lexical scope projection, and fallback resolution. - Extensible Standard Modules: Conditionals, loops, text, show, model, events, and ref are discrete unprivileged modules that can be overridden or omitted, with
partials()provided as a modular wrapper for the built-in composition capability. - Extensible Module API: Create custom domain directives using
defineModule()and compose custom runtimes usingcreateEngine(). - Path-Based Reactive Store: Fine-grained reactive state with prefix-tree indexing, batch updates, computed properties, and prototype-pollution guards.
- Keyed DOM Reconciliation: Reactive
<for data-live>loops support Longest Increasing Subsequence (LCS) diffing, preserving DOM identity and focus state. - Strict CSP / Eval-Free: Operates with
Content-Security-Policy: script-src 'self'. All paths and handler names are identifier lookups, never evaluated JavaScript expressions. - Lightweight Production Bundle: 55.0 kB minified ESM bundle (
dist/index.min.js) with on-demand development diagnostics.
npm install lime-csr-jsFor development, npm run verify runs lint, type checks, tests, and the build.
For the complete release gate, use npm run verify:release; it also validates
real Chromium, examples, security regressions, the packed npm consumer, and
the external TypeScript consumer.
Start with the progressive examples guide or open the examples index. Recommended first stops are the Counter, Forms, Keyed List, Partials & Slots, Custom Module, Todo, and Operations Dashboard.
npm test: Runs unit and regression test suites in Node.js using JSDOM.npm run test:browser: Runs ESM bundle smoke tests in Node.js using JSDOM against built artifacts indist/.npm run test:csp: Validates runtime execution and DOM visibility under strict CSP (default-src 'none'; script-src 'self'; style-src 'self') in real headless Chromium/Chrome after building. Requires an installed browser (chromium,chromium-browser, orgoogle-chrome), or an explicitCHROMIUM_BINexecutable path.npm run test:examples: Discovers examples, parses module scripts, checks local imports/assets/templates, and rejects stale or unsafe patterns.npm run test:examples:browser: Runs representative examples in real Chromium and checks interaction results and browser errors.npm run test:security:browser: Runs permanent real-Chromium checks for executable attributes, mixed-case event attributes,srcdoc, unsafe URLs, and payload execution.npm run test:package: Packs the project, installs the tarball in a temporary external project, and checks root, core, modules, granular, and dist exports.npm run test:package:types: Runstscagainst a temporary external TypeScript consumer using the installed tarball and current JSDoc surface.npm run benchmark:release: Prints informational Store/text/keyed/model workload medians without enforcing unstable hard timing limits.
Chromium is the required real-browser release gate. npm run test:examples:firefox
is an optional exploratory smoke command when a reliable headless Firefox
installation is available; WebKit is not bundled or required by this release.
lime-csr-js@0.6.4 provides clean, dedicated subpaths:
// 1. Root package (Facade, Store, Diagnostics, Standard Modules)
import { mount, unmount, render, createStore, defineModule, createEngine } from 'lime-csr-js';
// 2. Kernel primitives only (alternative direct subpath import)
// import { createEngine, defineModule, attr, attrs, tag, pattern, createScope } from 'lime-csr-js/core';
// 3. All standard modules
import { partials, conditionals, loops, text, show, model, events, ref } from 'lime-csr-js/modules';
// 4. Granular single-module imports (for custom tree-shaken engines)
// import show from 'lime-csr-js/modules/show';
// import text from 'lime-csr-js/modules/text';
// import ref from 'lime-csr-js/modules/ref';
// 5. Minified browser bundle
import 'lime-csr-js/dist/index.min.js';Zero build tools or installation required. Lime can be loaded directly from jsDelivr's GitHub CDN in any modern browser via native <script type="module">.
If you want the complete framework with all directives pre-registered:
<script type="module">
import { createStore, mount } from 'https://cdn.jsdelivr.net/gh/mhmtsnmzkanly/lime-csr-js@v0.6.4/dist/index.min.js';
</script>If you only need specific directives (e.g. only text and events for a tiny widget), you can load the lightweight Micro-Kernel and only the individual module files you need:
<script type="module">
import { createEngine } from 'https://cdn.jsdelivr.net/gh/mhmtsnmzkanly/lime-csr-js@v0.6.4/dist/core.min.js';
import { createStore } from 'https://cdn.jsdelivr.net/gh/mhmtsnmzkanly/lime-csr-js@v0.6.4/dist/store.min.js';
import text from 'https://cdn.jsdelivr.net/gh/mhmtsnmzkanly/lime-csr-js@v0.6.4/dist/modules/text.min.js';
import events from 'https://cdn.jsdelivr.net/gh/mhmtsnmzkanly/lime-csr-js@v0.6.4/dist/modules/events.min.js';
// Assemble a bespoke engine with only the modules you need
const engine = createEngine({
modules: [text(), events()]
});
const store = createStore({ count: 0 });
engine.mount({
target: document.getElementById('app'),
template: 'counter',
store,
handlers: {
increment: () => store.update('count', (n) => n + 1),
}
});
</script>| Distribution File | Description | Typical Size |
|---|---|---|
dist/index.min.js |
Full bundle: Micro-Kernel, Store, Router, and all 8 standard modules | ~55.0 kB |
dist/core.min.js |
Micro-Kernel runtime: createEngine, defineModule, triggers, scope |
~35.0 kB |
dist/store.min.js |
Reactive Store: createStore, getByPath, setByPath |
~9.3 kB |
dist/router.min.js |
Compiled Trigger Router: createRouter |
~8.4 kB |
dist/modules/index.min.js |
All Standard Modules in one package | ~32.0 kB |
dist/modules/text.min.js |
data-text & {attr} template reactive bindings |
~3.8 kB |
dist/modules/show.min.js |
data-show reactive visibility toggle |
~2.1 kB |
dist/modules/events.min.js |
data-on-{event} delegated event dispatching |
~6.2 kB |
dist/modules/model.min.js |
data-model two-way form input binding |
~2.6 kB |
dist/modules/conditionals.min.js |
<if>, <else>, static/live condition evaluation |
~14.0 kB |
dist/modules/loops.min.js |
<for>, keyed list diffing, prototypal item scopes |
~17.0 kB |
dist/modules/partials.min.js |
Modular wrapper for built-in <partial> & <slot> composition |
~13.0 kB |
dist/modules/ref.min.js |
data-ref element and element collection references |
~1.5 kB |
Create an HTML file with a <template> and mount it using native ES modules:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Lime Counter</title>
</head>
<body>
<!-- 1. Declarative HTML Template -->
<template id="tpl-counter">
<div class="counter-card">
<h2>Counter: <span data-text="count"></span></h2>
<button data-on-click="increment">+1</button>
<button data-on-click="decrement">-1</button>
<p data-show="isPositive">Great! The count is positive.</p>
</div>
</template>
<!-- 2. Mount Target Container -->
<main id="app"></main>
<!-- 3. Reactive Logic -->
<script type="module">
import { createStore, mount } from 'lime-csr-js';
// Initialize reactive store
const store = createStore({ count: 0, isPositive: false });
// Computed property: updates automatically when count changes
store.computed('isPositive', ['count'], (count) => count > 0);
// Mount template into target (CSS selector or DOM Element)
const instance = mount({
target: '#app',
template: 'counter',
store,
handlers: {
increment() {
store.update('count', (c) => c + 1);
},
decrement() {
store.update('count', (c) => c - 1);
},
},
});
// To unmount and clear content later:
// unmount(instance); // or unmount('#app');
</script>
</body>
</html>The store provides path-based reactive state with upward and downward change notification:
import { createStore } from 'lime-csr-js';
const store = createStore({
user: {
profile: { name: 'Alice', age: 30 },
},
todos: [
{ id: 1, title: 'Learn Kernel Architecture', done: true },
{ id: 2, title: 'Write Custom Module', done: false },
],
});
// Read state via dotted path
console.log(store.get('user.profile.name')); // "Alice"
// Subscribe to path changes
const unsubscribe = store.subscribe('user.profile.name', (newVal, oldVal) => {
console.log(`Name changed from ${oldVal} to ${newVal}`);
});
// Write state (triggers subscribers)
store.set('user.profile.name', 'Bob');
// Batch updates: coalesces notifications into a single flush wave
store.batch(() => {
store.set('user.profile.name', 'Charlie');
store.set('user.profile.age', 31);
}); // Subscribers fire once here
// Unsubscribe
unsubscribe();Diagnostics can be observed globally or per mount:
const instance = mount({
target: '#app',
template: 'dashboard',
store,
onError(diagnostic) {
monitoring.captureException(diagnostic.details.error, {
tags: { code: diagnostic.code, category: diagnostic.category },
});
},
});Every diagnostic includes code, message, context, severity, category, details, timestamp, and count. onDiagnostic receives warnings and errors for its mount target; onError receives only errors. Equivalent repeated diagnostics within one second are aggregated into the first event by incrementing count.
Lime's single extension mechanism is the Module. A module specifies trigger hooks that run in either the Transform phase (structural compilation) or the Link phase (behavioral attachment):
import { defineModule, attr } from 'lime-csr-js';
// Define a custom tooltip module
export const tooltipModule = defineModule({
name: 'tooltip',
triggers: [
attr('data-tooltip', {
phase: 'link',
// 1. Pure read step: extracts configuration once during link setup
read(el, ctx) {
return { text: el.getAttribute('data-tooltip') };
},
// 2. Setup step: binds listeners or watches store
setup(el, data, ctx) {
if (!data || !data.text) return;
const showTooltip = () => {
el.setAttribute('title', data.text);
};
el.addEventListener('mouseenter', showTooltip);
// Register teardown on the unified LIFO cleanup stack
ctx.onCleanup(() => {
el.removeEventListener('mouseenter', showTooltip);
});
},
}),
],
});The default mount() and render() functions use a built-in engine with all 8 standard modules. You can build a customized, isolated runtime using createEngine():
import { createEngine, createStore } from 'lime-csr-js';
import { text, show, events } from 'lime-csr-js/modules';
import { tooltipModule } from './tooltip-module.js';
// Compose an engine with explicit module precedence:
// Earlier modules take precedence over later modules for conflicting triggers.
const engine = createEngine({
modules: [
tooltipModule, // Custom module runs first
text(),
show(),
events(),
],
});
// Mount with custom engine
const store = createStore({ message: 'Hello World' });
const instance = engine.mount({
target: document.getElementById('app'),
template: 'my-template',
store,
});Engines are isolated and may run independently on separate target elements. A target element has one active mount owner across all engines: mounting another engine to the same target automatically unmounts the previous runtime before the replacement starts. This keeps DOM updates, reactive subscriptions, and delegated event listeners owned by one runtime at a time.
| Module | Phase | Triggers | Description |
|---|---|---|---|
| partials | Transform | <partial name="..." data="..."> |
Modular wrapper for built-in template composition with isolated scopes, named/default <slot> projection, fallback content, and caller scope preservation. |
| conditionals | Transform + Link | <if>, <template data-if>, <else> |
Evaluates comparison operators (is-gt, is-lt, is-gte, is-lte, is-eq, is-neq, is-truthy). data-live provides reactive updates. |
| loops | Transform + Link | <for each as>, <template data-for> |
Renders array items with prototypal child scopes. data-live key="..." provides keyed LCS reconciliation. |
| text | Link | data-text="path", {x} attribute templates |
Reactively binds store values to textContent and attribute values with URL sanitization. |
| show | Link | data-show="path" |
Toggles element visibility via the native hidden attribute without altering inline styles. |
| model | Link | data-model="path", data-model-group="prefix" |
Two-way binding for inputs, checkbox arrays (path[]), contenteditable, and form groups with modifiers (.lazy, .trim, .number, .debounce-<ms>) and initial DOM fallback. |
| events | Link | data-on-{event}="handler", data-on-*-data |
Delegated event dispatch with single object payload { event, element, scope, store, data, refs }, companion data attributes, and prototype protection. |
| ref | Link | data-ref="name" |
Collects DOM element references into app.refs and passes refs into event handler payloads. Supports [] array suffixes and duplicate grouping. |
Lime enforces a strict separation of concerns:
- Kernel = Mechanism: Implements generic trigger matching, route compilation, deterministic precedence, prototypal scope inheritance, unified LIFO cleanup stack, and diagnostic dispatch. The kernel has zero knowledge of
if,for, or any specific attribute names. - Module = Behavior: All syntax and rendering semantics are encapsulated in discrete module definitions.
HTML Template
↓
Phase 1: Transform (Fixed-Point Loop)
- Partials expansion (<partial>)
- Loop unrolling (<for>)
- Conditional branching (<if>/<else>)
- Static ${path} interpolation
↓
Phase 2: Link (Single-Pass Traversal)
- Two-way form binding (data-model)
- Reactive text & attributes (data-text, {x})
- Visibility toggling (data-show)
- Live conditionals & live loops reactive setup
- Delegated event listeners (data-on-*)
- DOM element references (data-ref)
↓
Connected DOM with LIFO Cleanup Stack
- No Expression Evaluation: Lime never uses
eval(),new Function(), or dynamic code generation. - CSP Compatibility: Fully compliant with strict
script-src 'self'policies. - Prototype Pollution Guard:
store.set()andgetByPath()reject__proto__,constructor, andprototypepath segments. - URL Protocol Sanitizer:
href,src,action, and other URL attributes only accepthttp:,https:, root-relative (/), or anchor (#) values. Dangerous schemes (javascript:,data:,vbscript:) are neutralized. - DOM API Safety: Text is assigned via
textContent, avoiding raw HTML interpretation.
- Plugin API Removed: The legacy
definePlugin()andPLUGIN_API_VERSIONare removed. UsedefineModule()andcreateEngine(). - Monolithic Helpers Removed: Internal renderer functions (
setupBindings,expandLoops,processAllIfs, etc.) are no longer exposed on the root package. Use the standard modules or facaderender()/mount(). - Root Public Exports Cleaned: Root package exports exactly 34 clean symbols (Facade, Store, Diagnostics, Kernel Primitives, and Standard Modules).
For full migration instructions and before/after code examples, see DOCS.md: Migration Guide.
- DOCS.md: Comprehensive technical reference manual covering architecture, triggers, lifecycle, store, scope, standard modules, error codes, and troubleshooting.
- llms.txt: Compact, high-density reference optimized for LLM prompting and context windows.
- llms-full.txt: Exhaustive machine-readable reference containing the complete public API and implementation semantics.
- CHANGELOG.md: Version release notes and breaking changes log.
MIT License — see LICENCE.md.