npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@ebinasoft/xeval

v5.3.0

Published

Dynamic script, HTML & CSS injection library with template engine

Readme

xeval

Inject JavaScript, HTML & CSS dynamically into the DOM — with a built-in template engine, smart caching, and full TypeScript support.

npm version bundle size TypeScript license maintained by ebinasoft

Get Started · API Reference · Examples · Contributing


Why xeval?

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' } })

✨ Features

  • 🧩 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 insertionappend, prepend, before, after, or replace
  • 🔄 Live update — modify injected HTML and CSS without re-injecting
  • 📦 Smart cacheloadFrom() caches remote files with optional TTL and stale fallback
  • 🔔 Dual callbacksonInject at engine level and per run() call
  • 🔒 Safe mode — use textContent instead of innerHTML for untrusted content
  • 🧹 Clean DOMcleanup() and cleanupOne(key) remove exactly what was injected
  • 💎 Full TypeScript — strict types, exported interfaces, autocompletion everywhere
  • 🪶 Zero dependencies — browser-native APIs only

📦 Installation

npm install @ebinasoft/xeval
yarn add @ebinasoft/xeval
pnpm add @ebinasoft/xeval

Via CDN — no install needed:

<script type="module">
  import xeval from 'https://cdn.jsdelivr.net/npm/@ebinasoft/xeval/dist/xeval.esm.js'
</script>

🚀 Quick Start

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' } })

📖 API Reference

xeval — Entry Point

| 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 |


CoreEngine — Shared API

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 |


ScriptEngine

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 on ScriptEngine — once a script runs, its DOM element is inert. Use cleanup() + run() to re-execute.


HtmlEngine

engine.run(options?: HtmlRunOptions): HTMLDivElement
engine.inject(options?: HtmlRunOptions): HTMLDivElement          // alias
engine.update(options?: HtmlUpdateOptions): Element | null

run() 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 |


CSSEngine

engine.run(options?: CssRunOptions): HTMLStyleElement
engine.inject(options?: CssRunOptions): HTMLStyleElement        // alias
engine.update(options?: CssUpdateOptions): HTMLStyleElement | null

run() 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 |


Template Engine — $$placeholders

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

loadFrom() — Remote files with cache

// 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 everything

If 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.


onInject — Dual callback system

// 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..."

🌍 Real-world Examples

Plugin system

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' } })
}

Live theme switcher

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' }
  })
})

Consent-gated third-party scripts

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' } })
  }
})

Reversible debug mode

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()
  }
})

Dynamic component rendering

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())

🏗️ Architecture

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 })

📁 Project Structure

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

🤝 Contributing

xeval is a passion project by @ThePrinceTrueface, maintained under the ebinasoft organization. Contributions, ideas, and feedback are genuinely welcome.

How to contribute

# 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 typecheck

Commit convention

This 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 README

Types: feat · fix · refactor · docs · chore · test · perf

What we'd love help with

  • 🧪 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

🛡️ Security

xeval injects and executes arbitrary content into the page context. A few guidelines:

  • Never pass untrusted user input directly as script source
  • Use safe: true on HtmlEngine when injecting user-generated content
  • For full isolation, consider an <iframe sandbox> as an alternative

📄 License

MIT © ThePrinceTruefaceebinasoft


If xeval saved you time or sparked an idea, a ⭐ on GitHub means the world.

Made with passion by @ThePrinceTrueface · ebinasoft