jq79

jq79 logo

npm coverage esm cdn

A independent reactive component library that ships as a single file. Svelte-style reactive scripts, fine-grained DOM updates via proxy-based dependency tracking.

No compiler, no bundler, no dependencies. A component is a .html file — the browser already knows how to fetch one. Drop the library from a CDN, serve your components from any static host, and the page works. No npm install, no build step, no config.

Take the tutorial → — a handful of exercises you edit in the browser, with a live preview. Because there's no compiler, the tutorial runs the real library: the component you write is the component that mounts.

No build step

A component is a .html file. The browser already knows how to fetch one — so nothing has to happen to your components between writing them and serving them:

<!doctype html>
<div id="app"></div>

<script type="module">
  import { C79 } from "https://esm.sh/jq79"

  C79.fetch("./app.html").mount("#app", { title: "Today" })
</script>

That is the whole deployment. The library from a CDN, the component from your own host, no build step in between. C79 is Component79 under a shorter name — the same class, the same API.

While you're writing them, npx jq79 dev serves that folder and hot-reloads the components you edit, keeping their state — no build step there either. See the dev server.

Installation

npm

npm install jq79
import { Component79, C79, $, $$, $reactive, $computed, $toRaw, parseComponent } from "jq79"

Vite

With the bundled plugin, .html component files import as modules — no runtime fetch, with HMR in dev:

// vite.config.js
import { jq79 } from "jq79/vite"
export default { plugins: [jq79()] }
import UserCard from "./UserCard.html"
UserCard.mount("#app")

The plugin is a pure loader (nothing inside the component is transformed), so the same file keeps working from public/ via Component79.fetch — see the Vite plugin docs.

CDN

Once published to npm, the package is automatically served by every major CDN — no separate publishing step:

<!-- as an ES module -->
<script type="module">
  import { Component79 } from "https://esm.sh/jq79"
  // or: https://cdn.jsdelivr.net/npm/jq79/+esm
  // or: https://unpkg.com/jq79?module
  // or, straight from this repo's GitHub Pages (latest release):
  //     https://jgermade.github.io/jq79/jq79.js
</script>

<!-- or as a classic script exposing window.jq79 -->
<script src="https://cdn.jsdelivr.net/npm/jq79/dist/jq79.global.js"></script>
<!-- or: <script src="https://jgermade.github.io/jq79/jq79.global.js"></script> -->
<script>
  const { Component79 } = jq79
</script>

Pin a version in production: https://cdn.jsdelivr.net/npm/jq79@0.1.0/... (the GitHub Pages copy always tracks the latest release).

Which is enough for a whole page: the library from a CDN, the component from your own host, no build step in between. C79 (short for Component79, the same class) hands back a pending component you can mount right away:

<main id="app"></main>

<script type="module">
  import { C79 } from "https://jgermade.github.io/jq79/jq79.js"

  C79
    .fetch("./app.html")
    .mount("#app")
</script>

fetchAll([...]) fetches several at once instead. See loading remote components.

The library also exports parseComponent(source) as a shorthand for new Component79(source), and enableHotReload() / hotUpdate(filename, src) for custom dev setups — see the dev server.

The source is small enough to read in a sitting: the core (parsing, rendering, components) lives in src/jq79.ts, with three leaf helpers — dom.ts, reactive.ts and transform.ts. The published build is a single dependency-free file.

Quick start

From a CDN (no build step)

<!doctype html>
<div id="app"></div>

<script type="module">
  import { C79 } from "https://esm.sh/jq79"

  C79.fetch("./app.html").mount("#app", { title: "Today" })
</script>

The component file is served as-is from your host — no bundler, no config.

From npm

import { Component79 } from "jq79"

const jq79 = new Component79(html`
  <script :setup>
    let firstName = null
    let lastName = null
    $: fullName = firstName && lastName ? \`\${firstName} \${lastName}\` : ""

    API.fetch("/me").then(user => {
      firstName = user.firstName
      lastName = user.lastName
    })
  </script>

  <div :if="fullName" class="user-info">
    <span>{{ fullName }}</span>
  </div>

  <style>
    .user-info { color: rebeccapurple; }
  </style>
`)

jq79.mount("#app")

When the fetch resolves, the assignments to firstName/lastName re-run the $: declaration, which flips the :if and renders the span — no manual wiring.

Documentation

License

ISC