Inject JavaScript, HTML & CSS dynamically into the DOM — with a built-in template engine, smart caching, and full TypeScript support.
Get Started · API Reference · Examples · Contributing
Most apps eventually need to run or inject dynamic content at runtime — a plugin loaded from a server, a theme switched by the user, a script that depends on runtime config. The native options (eval, innerHTML, manual script tags) are verbose, unsafe by default, and offer no structure.
xeval gives you a clean, typed, chainable API to do all of this — without a bundler, without a framework, without boilerplate.
import xeval from '@ebinasoft/xeval'
// Inject a dynamic script
xeval.prepare(`console.log("Hello, $$name!")`)
.run({ context: { name: 'world' } })
// Inject HTML with a template
xeval.prepareHTML(`<h1 class="title">$$heading</h1>`)
.onInject((el, key) => el.classList.add('visible'))
.run({ target: '#app', context: { heading: 'Welcome' } })
// Inject CSS with live update
const theme = xeval.prepareCSS(`body { background: $$bg; color: $$fg; }`)
theme.run({ context: { bg: '#0f0f0f', fg: '#ffffff' }, id: 'app-theme' })
theme.update({ context: { bg: '#ffffff', fg: '#0f0f0f' } })- 🧩 Three engines — inject JS, HTML, and CSS with a unified API
- 🔑 Unique injection keys — every element gets a
data-xeval-key(UUID) for precise targeting - 🎯 Flexible insertion —
append,prepend,before,after, orreplace - 🔄 Live update — modify injected HTML and CSS without re-injecting
- 📦 Smart cache —
loadFrom()caches remote files with optional TTL and stale fallback - 🔔 Dual callbacks —
onInjectat engine level and perrun()call - 🔒 Safe mode — use
textContentinstead ofinnerHTMLfor untrusted content - 🧹 Clean DOM —
cleanup()andcleanupOne(key)remove exactly what was injected - 💎 Full TypeScript — strict types, exported interfaces, autocompletion everywhere
- 🪶 Zero dependencies — browser-native APIs only
npm install @ebinasoft/xevalyarn add @ebinasoft/xevalpnpm add @ebinasoft/xevalVia CDN — no install needed:
<script type="module">
import xeval from 'https://cdn.jsdelivr.net/npm/@ebinasoft/xeval/dist/xeval.esm.js'
</script>import xeval from '@ebinasoft/xeval'
// ── JS ────────────────────────────────────────
xeval.prepare(`
const user = $$user
document.title = "Welcome, " + user.name
`).run({
context: { user: { name: 'Alice', role: 'admin' } }
})
// ── HTML ──────────────────────────────────────
const card = xeval.prepareHTML(`
<div class="card">
<h2>$$title</h2>
<p>$$description</p>
</div>
`)
const el = card.run({
target: '#container',
position: 'append',
context: { title: 'Hello', description: 'xeval is awesome' },
onInject: (el, key) => console.log('Injected with key:', key)
})
// Update it later without re-injecting
card.update({ context: { title: 'Updated!', description: 'Still the same element.' } })
// ── CSS ───────────────────────────────────────
const theme = xeval.prepareCSS(`
:root {
--color-bg: $$bg;
--color-text: $$text;
--font-size: $$size;
}
`)
theme.run({ context: { bg: '#1a1a2e', text: '#eee', size: '16px' }, id: 'theme' })
// Switch theme on the fly
theme.update({ context: { bg: '#ffffff', text: '#111', size: '16px' } })
// ── Remote files ──────────────────────────────
const engine = await xeval.loadFrom('/plugins/analytics.js')
engine.run({ context: { trackingId: 'UA-XXXXX' } })| Method | Returns | Description |
|---|---|---|
prepare(source) |
ScriptEngine |
Create a JS engine from a string |
prepareHTML(source) |
HtmlEngine |
Create an HTML engine from a string |
prepareCSS(source) |
CSSEngine |
Create a CSS engine from a string |
loadFrom(url, options?) |
Promise<Engine> |
Fetch a remote file and return the right engine |
clearCache(url?) |
void |
Clear one URL or the entire cache |
isCached(url) |
boolean |
Check if a URL is cached and not expired |
cacheInfo(url) |
object | null |
Get cache metadata for a URL |
All engines inherit these methods and properties.
| Member | Type | Description |
|---|---|---|
onInject(callback) |
this |
Register an engine-level callback — chainable |
getByKey(key) |
Element | null |
Retrieve an injected element by its key |
cleanupOne(key) |
boolean |
Remove a single injection from the DOM |
cleanup() |
void |
Remove all injections from the DOM |
render(options?) |
string |
Preview interpolated source without injecting |
rawSource |
string |
The original uninterpolated source |
lastKey |
string | null |
Key of the last injection |
lastInjected |
Element | null |
Last injected element |
keys |
string[] |
All injection keys in order |
engine.run(options?: ScriptRunOptions): HTMLScriptElement
engine.inject(options?: ScriptRunOptions): HTMLScriptElement // alias| Option | Type | Default | Description |
|---|---|---|---|
context |
Context |
— | Values for $$placeholder interpolation |
target |
string | Element |
document.body |
Where to append the script |
module |
boolean |
false |
Inject as type="module" |
id |
string |
— | Set an id on the script element |
onInject |
InjectCallback |
— | Callback fired after this injection |
Note:
update()is not available onScriptEngine— once a script runs, its DOM element is inert. Usecleanup()+run()to re-execute.
engine.run(options?: HtmlRunOptions): HTMLDivElement
engine.inject(options?: HtmlRunOptions): HTMLDivElement // alias
engine.update(options?: HtmlUpdateOptions): Element | nullrun() options:
| Option | Type | Default | Description |
|---|---|---|---|
context |
Context |
— | Values for $$placeholder interpolation |
target |
string | Element |
document.body |
Insertion container |
position |
InsertPosition |
'append' |
Where to insert relative to target |
safe |
boolean |
false |
Use textContent instead of innerHTML |
id |
string |
— | Set an id on the wrapper element |
class |
string |
— | Set a class on the wrapper element |
onInject |
InjectCallback |
— | Callback fired after this injection |
update() options:
| Option | Type | Description |
|---|---|---|
context |
Context |
New values for interpolation |
key |
string |
Target a specific injection by key |
id |
string |
Target a specific injection by id |
safe |
boolean |
Use textContent instead of innerHTML |
InsertPosition values:
| Value | Behavior |
|---|---|
'append' |
Add as last child of target (default) |
'prepend' |
Add as first child of target |
'before' |
Insert before the target element |
'after' |
Insert after the target element |
'replace' |
Replace target's content entirely |
engine.run(options?: CssRunOptions): HTMLStyleElement
engine.inject(options?: CssRunOptions): HTMLStyleElement // alias
engine.update(options?: CssUpdateOptions): HTMLStyleElement | nullrun() options:
| Option | Type | Default | Description |
|---|---|---|---|
context |
Context |
— | Values for $$placeholder interpolation |
target |
string | Element |
document.head |
Where to append the style element |
id |
string |
— | Set an id on the style element |
media |
string |
— | Set the media attribute (e.g. 'print', 'screen') |
onInject |
InjectCallback |
— | Callback fired after this injection |
xeval's template engine replaces $$key placeholders in your source with values from the context object.
// Primitives — replaced as-is
xeval.prepare(`const x = $$value`).run({ context: { value: 42 } })
// → const x = 42
// Strings — wrap in quotes yourself for JS, xeval handles HTML/CSS
xeval.prepare(`const lang = "$$lang"`).run({ context: { lang: 'fr' } })
// → const lang = "fr"
// Objects & Arrays — automatically JSON.stringify'd
xeval.prepare(`const user = $$user`).run({
context: { user: { id: 1, name: 'Alice' } }
})
// → const user = {"id":1,"name":"Alice"}
// Functions — serialized as arrow function const declarations
xeval.prepare(`$$greet\ngreet()`).run({
context: { greet: function() { alert('hi') } }
})
// → const greet = () => { alert('hi') }
// greet()
// Unknown keys — left untouched
xeval.prepare(`const x = $$unknown`).run({ context: {} })
// → const x = $$unknown// Auto-detect type from extension
const jsEngine = await xeval.loadFrom('/plugin.js')
const htmlEngine = await xeval.loadFrom('/template.html')
const cssEngine = await xeval.loadFrom('/theme.css')
// Force type when URL has no extension
const engine = await xeval.loadFrom('/api/template', { type: 'html' })
// Set a cache TTL (milliseconds)
const engine2 = await xeval.loadFrom('/theme.css', { ttl: 5 * 60 * 1000 })
// Cache helpers
xeval.isCached('/plugin.js') // → true | false
xeval.cacheInfo('/plugin.js') // → { cachedAt, ttl, type }
xeval.clearCache('/plugin.js') // clear one URL
xeval.clearCache() // clear everythingIf a fetch fails and a stale cache entry exists, xeval automatically serves it as a fallback with a warning — keeping your app resilient to network hiccups.
// Engine-level — fires on every run() call, chainable
const engine = xeval.prepareHTML(`<p>$$text</p>`)
.onInject((el, key) => {
el.classList.add('fade-in')
console.log('Engine callback — key:', key)
})
// run()-level — fires only for this specific injection
engine.run({
context: { text: 'Hello' },
onInject: (el, key) => console.log('Run callback — key:', key)
})
// Callback order:
// 1. run() callback → "Run callback — key: abc-123..."
// 2. engine callback → "Engine callback — key: abc-123..."const plugins = ['/plugins/logger.js', '/plugins/analytics.js', '/plugins/chat.js']
for (const url of plugins) {
const engine = await xeval.loadFrom(url)
engine.run({ context: { env: 'production' } })
}const theme = xeval.prepareCSS(`
body {
--bg: $$bg;
--text: $$text;
--accent: $$accent;
}
`)
theme.run({ id: 'app-theme', context: { bg: '#fff', text: '#111', accent: '#6c47ff' } })
document.querySelector('#dark-mode').addEventListener('change', (e) => {
theme.update({
context: e.target.checked
? { bg: '#0f0f0f', text: '#eee', accent: '#a78bfa' }
: { bg: '#fff', text: '#111', accent: '#6c47ff' }
})
})cookieBanner.onAccept(async (categories) => {
if (categories.analytics) {
const engine = await xeval.loadFrom('/vendors/gtag.js')
engine.run({ context: { trackingId: 'UA-XXXXX' } })
}
if (categories.support) {
const engine = await xeval.loadFrom('/vendors/intercom.js')
engine.run({ context: { appId: 'APP_ID' } })
}
})const debug = xeval.prepare(`
window.__debug = true
console.log('[debug] mode activated')
document.body.dataset.debug = 'true'
`)
document.querySelector('#debug-toggle').addEventListener('change', (e) => {
if (e.target.checked) {
debug.run({ id: 'debug-mode' })
} else {
debug.cleanup()
}
})const userCard = xeval.prepareHTML(`
<div class="user-card" data-role="$$role">
<img src="$$avatar" alt="$$name" />
<h3>$$name</h3>
<span class="badge">$$role</span>
</div>
`)
// Render all users
users.forEach(user => {
userCard.run({
target: '#user-list',
position: 'append',
context: user,
onInject: (el, key) => {
el.addEventListener('click', () => openProfile(user.id))
}
})
})
// Clean up when navigating away
window.addEventListener('popstate', () => userCard.cleanup())Xeval (singleton)
├── prepare(source) → ScriptEngine
├── prepareHTML(source) → HtmlEngine
├── prepareCSS(source) → CSSEngine
└── loadFrom(url, options) → ScriptEngine | HtmlEngine | CSSEngine
(with built-in cache)
CoreEngine (abstract base class)
├── _interpolate() Template engine — $$key substitution
├── _stamp() Generates & assigns data-xeval-key (UUID)
├── _fireInject() Dispatches run() then engine callbacks
├── onInject() Register engine-level callback (chainable)
├── getByKey() Retrieve element by key
├── cleanupOne() Remove one injection by key
├── cleanup() Remove all injections
├── render() Preview without DOM injection
├── rawSource Original source string
├── lastKey / lastInjected / keys
│
├── ScriptEngine → run({ context, module, id, target, onInject })
├── HtmlEngine → run({ context, target, position, safe, id, class, onInject })
│ update({ context, key, id, safe })
└── CSSEngine → run({ context, target, id, media, onInject })
update({ context, key, id })
xeval/
├── src/
│ └── xeval.ts ← single source file
├── dist/
│ ├── xeval.esm.js ← ES Module build
│ ├── xeval.cjs.js ← CommonJS build
│ ├── xeval.min.js ← Minified CDN build
│ └── xeval.d.ts ← TypeScript declarations
├── tests/
│ ├── core.test.ts
│ ├── script.test.ts
│ ├── html.test.ts
│ └── css.test.ts
├── CHANGELOG.md
├── README.md
└── package.json
xeval is a passion project by @ThePrinceTrueface, maintained under the ebinasoft organization. Contributions, ideas, and feedback are genuinely welcome.
# 1. Fork the repo and clone it
git clone https://github.com/YOUR_USERNAME/xeval.git
cd xeval
# 2. Install dependencies
npm install
# 3. Start dev mode (watch + rebuild on save)
npm run dev
# 4. Typecheck
npm run typecheckThis project uses Conventional Commits:
feat(html): add onInject callback option
fix(core): getByKey returns null instead of undefined
refactor(cache): extract buildEngine as private method
docs: update loadFrom examples in READMETypes: feat · fix · refactor · docs · chore · test · perf
- 🧪 Tests — Vitest unit tests for all engines
- 📖 Examples — real-world use cases in
docs/examples/ - 🐛 Bug reports — open an issue with a minimal reproduction
- 💡 Ideas — open a discussion before implementing big features
xeval injects and executes arbitrary content into the page context. A few guidelines:
- Never pass untrusted user input directly as script source
- Use
safe: trueonHtmlEnginewhen injecting user-generated content - For full isolation, consider an
<iframe sandbox>as an alternative
MIT © ThePrinceTrueface — ebinasoft
If xeval saved you time or sparked an idea, a ⭐ on GitHub means the world.
Made with passion by @ThePrinceTrueface · ebinasoft