Wely

Lightweight Web Component framework — one defineComponent(), no class syntax, no framework lock-in.

Wely core powers rendering internally and is never exposed. Developers interact exclusively through the Wely API. Output is native custom elements — fully portable, they run in plain HTML, React, Vue, Angular, Svelte, anywhere.

Single-page documentation. This site is one static HTML file: jump with the nav or read top to bottom. Run wely page from the project root; if docs/index.html is missing, the CLI creates a minimal shell once. The PokéAPI demo below uses the bundle published to docs/assets/wely.bundle.umd.js — existing doc HTML is never overwritten.

Why Wely?

Building frontend components today typically means choosing a heavy framework and wiring up a dozen tools. Wely replaces that with a single unified toolkit.

The problem

How Wely solves it

StepWhat you doWhat Wely handles
Configurewely setup or wely initwely.config.ts, package.json (wely dev / wely build, ^welyjs, autoComponents)Centralized config via ctx.config
Diagnosewely doctorNode, welyjs, componentsDir, dist, config checks with fix hints
Createwely create w-card --props title:String --testScaffolds component file and barrel index
Developwely devHot-reloading playground; auto-discovers components when wely.autoComponents is on
StyleUse Tailwind classes in templatesCompiled and injected into Shadow DOM
FetchcreateClient({ baseURL })HTTP client with interceptors, timeout
Statectx.resource() / ctx.use(store)Async resources + shared stores
Testwely test · wely test --changed · wely ciVitest + jsdom; bundled default config when no local vitest.config / vite.config
Buildwely buildES + UMD bundles; auto-discovers components from componentsDir by default
Embedwely embed · wely add react|vuePlain HTML scaffold and framework integration snippets
Exportwely export ../other-project/libCopies output to any folder
Documentwely docs · wely docs --watchGenerates COMPONENTS.md

One CLI, one config file — from scaffolding to production.

Bundle Size

Wely produces minimal bundles. Runtime includes the Wely core renderer, API utilities (defineComponent, store, resource, fetch), and Tailwind CSS. All sizes are minified + gzipped.

BuildSize (min+gzip)
Runtime only (wely.es.js)13 KB
1 component (w-button)13.3 KB
2 components (+ w-counter)13.6 KB
3 components (+ w-counter-card)14 KB
5 components (+ w-pokemon-grid, w-user-list)15 KB

Per-component overhead: ~0.4–0.5 KB for simple components.

Pay for what you use — Wely bundles only what you import. Add one component → ~13 KB. Add five → ~15 KB. No framework runtime at the consumer; output is native Web Components. Tree-shaking keeps the bundle minimal: unused components never land in the final file.

Quick Start

Fastest path (new project): one command scaffolds config, installs deps, adds a sample component + test, and runs the first build.

mkdir my-app && cd my-app
wely setup                   # init + install + w-demo component + build
wely dev                     # playground (or: npm run dev)
wely doctor                  # verify setup

Manual (step by step): Only wely.config.ts + welyjs — no vite.config, no extra deps.

mkdir my-app && cd my-app
wely init                    # package.json + wely.config + autoComponents + components/
npm install
wely create w-hello --props msg:String --test
wely build                   # auto-discovers components → dist/wely.bundle.*.js
wely dev                     # playground (or: npm run dev — same scripts after init)

Full repo (Wely development):

npm install
npm run dev      # playground at localhost:5173
npm run build    # library → dist/wely.es.js + dist/wely.umd.js
npm run test
npm run test:run

DX Quickstart

One-command setup, diagnosis, embed, and CI — all through the Wely CLI.

wely setup                   # scaffold + install + sample component + build
wely doctor                  # diagnose setup (wely doctor --json for CI)
wely build                   # respects wely.autoComponents in package.json
wely build --no-auto-components
wely embed                   # → html-usage/index.html (plain HTML usage)
wely add react               # integrations/react-example.tsx
wely add vue                 # integrations/vue-example.vue
wely test --changed          # only tests for git-changed components
wely docs --watch            # regenerate COMPONENTS.md on save
wely ci                      # build + test + docs + dist verify

package.jsonwely config:

{
  "wely": {
    "componentsDir": "src/wely-components",
    "autoComponents": true,
    "componentExclude": ["**/*.stories.ts"]
  }
}

When autoComponents is true (default after wely init), wely build and wely dev discover *.ts files under componentsDir — no manual src/bundle.ts import chain required.

Plain HTML script loading

wely embed scaffolds a page with defer on the bundle and wely.ready(tag) for safe boot. Use the same pattern when dropping a UMD bundle into any static page:

<script src="dist/wely.bundle.umd.js" defer></script>
<w-hello msg="World"></w-hello>
<script>
  wely.ready('w-hello').then(function () {
    // DOM parsed, component defined and upgraded
  });
</script>

Dynamic load: import { loadScript } from 'welyjs' then await loadScript('/dist/wely.bundle.umd.js').

Developer workflow

Typical loop for an app that lives in its own folder: scaffold → add or edit components → use the playground for quick feedback → build artifacts for production or static hosting.

1 · Bootstrap

wely setup or wely initnpm install

Creates wely.config.ts, package.json (wely.autoComponents: true), src/bundle.ts, src/wely-components/. Run wely doctor to verify.

2 · Components

wely create <tag> or edit *.ts under your components folder

With autoComponents, new files are picked up automatically — no manual index.ts imports required

3 · Try in the playground

wely dev (no local vite.config)

Gallery lists all registered components · Preview lab: HTML editor (highlighted), live preview, optional session restore · Hash routes #/gallery, #/preview?tag=…

4 · Build & ship

wely builddist/ bundle (and library modes when a vite.config exists)

wely embed for plain HTML · wely export <path> copies artifacts · wely page updates docs/assets/wely.bundle.umd.js · wely ci for local pipeline

Loop back from step 3 anytime: change components, save, HMR updates the playground. Step 4 is when you need a distributable bundle or a deployable folder.

At a glance (flow)

Unit tests (wely test)

Install vitest and jsdom as devDependencies (wely init adds them and a test script). wely test runs Vitest in watch mode; wely test --run runs once; wely test --changed focuses on git-changed component tests.

For a full local pipeline, run wely ci — build + test + docs generation + dist verification (wely ci --json for machine-readable output).

Import test primitives from welyjs/test (describe, it, expect, …) plus helpers like getComponentContext and withHost — single source of truth with Vitest under the hood.

If the project has no vitest.config.* and no vite.config.*, the CLI passes the published welyjs/vitest-consumer config so Vitest does not accidentally load a vite.config from a parent directory. Default globs: src/**/*.test.ts, src/**/*.spec.ts; environment jsdom; passWithNoTests: true. Add your own config when you need aliases, coverage, or different roots.

Dev mode & playground

wely dev starts a Vite dev server with hot reload. On first run (when no vite.config exists), the CLI uses the bundled vite.dev.config.ts: the published index.html shell and a virtual entry that imports your wely.config, dev CSS, then your components (via glob auto-discovery when wely.autoComponents is on, or src/bundle.ts when off), then mountApp(). If you already have a vite.config, plain vite runs instead.

Views (hash routes)

The UI is a single page with client-side routing via location.hash — no extra HTML files. Default route is #/home.

RoutePurpose
#/homeIntro, shortcuts to other views
#/docsCopy-paste ES module & UMD snippets, CLI cheat sheet
#/galleryAll components in searchable cards with live props
#/preview · #/preview?tag=w-buttonPreview lab: HTML editor with syntax highlighting (CodeMirror), Live preview (debounced) without overwriting your markup, one sandbox for multiple tags, props for the first registered element in document order, sessionStorage for snippet / filter. Apply now syncs the URL.

Screenshots

Captured from the Wely repo playground (npm run dev). Regenerate with npm run docs:playground-ss (runs scripts/capture-playground-screenshots.mjs).

Playground home: intro and buttons to browse components, preview lab, and integration docs
Home — entry screen with shortcuts
Playground docs tab: ES module example, UMD script example, CLI table
Docs — integration snippets and CLI reference
Playground components gallery: searchable list of component cards with props
Components — full gallery with search and collapse

Below: Preview with w-button selected. Two columns — tag filter + list; main pane has title, Props (edits push into the markup field), dashed stage, then markup textarea with Live preview so typing updates the preview after a short delay. You can wrap tags in extra HTML; the first matching registered custom element is used. Apply now applies immediately and syncs the hash. Narrow viewports may wrap the top nav.

Wely playground Preview: left sidebar with Filter tags field and tag buttons; main area shows w-button title, PROPS section with label variant disabled, dashed preview box, and HTML snippet textarea below
Preview lab — live markup toggle, props sync, full-page capture.

Where the playground lives

Wely repo (full checkout): index.html + src/playground/* + src/styles/tailwind.cssnpm run dev uses the same entry as below.

Consumer app (only wely init files, no vite.config): the CLI ships the shell HTML and a virtual entry that imports your wely.config.ts, generated dev CSS, welyjs/playground/app, and your components — you do not copy src/playground/ into your repo.

ArtifactPurpose
index.html (from welyjs package)Shell with #app; inline playground chrome styles
virtual:wely-playgroundEntry: config → dev CSS → mountApp() after dynamic import of components
virtual:wely-componentsWhen autoComponents: glob-imports componentsDir/**/*.ts (excludes tests, index). When off: imports src/bundle.ts
src/playground/app.tsNavigation, routes, views — resolved via welyjs/playground/app alias

Auto-rendering: getAllComponents() lists every registered component. New components from wely create show up immediately (HMR) when auto-discovery is enabled.

Interactive props: In the gallery (and preview lab), props get live inputs; attributes update the element and trigger re-renders.

Consumer projects without vite.config: vite.dev.config.ts serves the same HTML shell from the published index.html and a virtual entry that imports welyjs/playground/app after your config and components — same UX as the repo.

Defining a Component

Every component is a plain object passed to defineComponent():

import { defineComponent, html } from 'welyjs'

defineComponent({
  tag: 'w-counter',
  props: { start: Number },
  state() { return { count: 0 } },
  setup(ctx) { ctx.state.count = ctx.props.start ?? 0 },
  actions: {
    increment(ctx) { ctx.state.count++ },
    decrement(ctx) { ctx.state.count-- },
    reset(ctx) { ctx.state.count = ctx.props.start ?? 0 },
  },
  render(ctx) {
    return html`
      <button @click=${ctx.actions.decrement}>-</button>
      <span>${ctx.state.count}</span>
      <button @click=${ctx.actions.increment}>+</button>
      <button @click=${ctx.actions.reset}>Reset</button>
    `
  },
})

Use it: <w-counter start="5"></w-counter>

Component Composition

Wely components are native Custom Elements — nest them by using the tag name:

render(ctx) {
  return html`
    <div class="border rounded-lg p-4">
      <h3>${ctx.props.title}</h3>
      <w-counter start=${ctx.props.start ?? 0}></w-counter>
      <w-button label="Action" @w-click=${ctx.actions.onClick}></w-button>
    </div>
  `
}

Live Demo — PokéAPI

Fetching data with ctx.resource(). Data from PokéAPI.

API Surface

defineComponent(def)

Registers a native custom element. Fields: tag, props, devInfo, styles, state(), actions, setup(ctx), render(ctx), connected(ctx), disconnected(ctx). Actions receive (ctx, event?) — with @input, @click, etc., event.target gives the element.

The ctx object

PropertyDescription
ctx.elHost HTMLElement
ctx.propsReadonly attribute-synced properties
ctx.stateAuto-reactive state (mutations trigger re-render)
ctx.actionsBound action map — handlers receive (ctx, event); event.target for the element
ctx.update()Manually request re-render
ctx.emit(event, payload?)Dispatch CustomEvent
ctx.resource(fetcher, opts?)Async resource bound to lifecycle
ctx.use(store)Subscribe to shared store
ctx.configRead-only config from wely.config.ts

createClient(config?)

Zero-dependency HTTP client (native fetch). Axios-like: get, post, put, patch, delete, onRequest, onResponse, onError. Interceptors, timeout, query params, JSON.

createResource(fetcher, options?)

Async data primitive. Use via ctx.resource(). Tracks data, loading, error. Methods: fetch(), refetch(), abort(), mutate(), reset().

createStore(def)

Shared reactive state. state: () => ({...}), actions: { name(state, ...args) {...} }. Use ctx.use(store) to subscribe.

Configuration

Define in wely.config.ts with defineConfig(). Read with getConfig(), useConfig(key), or ctx.config. Supports import.meta.env.VITE_*.

Core Template API

html, css, nothing

devInfo — Dev tools attributes

When enabled (default), each component gets data-wely-version and data-wely-mounted attributes — visible in browser DevTools. Version comes from defineConfig({ version: '1.0.0' }) or per-component override.

// Default: attributes added
defineComponent({ tag: 'w-card', render: () => html`...` })

// Disable
defineComponent({ tag: 'w-secret', devInfo: false, render: () => html`...` })

// Override version per component
defineComponent({ tag: 'w-card', devInfo: { version: '2.0.0' }, render: () => html`...` })

Set version in wely.config.ts for global devInfo version.

componentsDir — Components folder

Override the default src/wely-components via package.json:

{
  "wely": {
    "componentsDir": "src/components",
    "autoComponents": true,
    "componentExclude": ["**/*.stories.ts"]
  }
}

autoComponents enables glob discovery on build and dev. componentExclude adds extra glob patterns to skip. Used by create, sync, list, docs, build, dev.

outDir — Build output folder

Override the default dist output directory via package.json:

{
  "wely": { "componentsDir": "src/components", "outDir": "build" }
}

Used by build, export, page. Both the library config and CLI respect this value.

CLI

Runs in the current directory. Use wely --help, wely -v, and wely help <command> for subcommands. New projects: wely setup (fastest) or wely initnpm installwely createwely build. Errors show what happened, how to fix, and a one-line command.

# One-shot setup
wely setup                   # init + install + sample component + build
wely setup --no-build        # skip initial build
wely doctor                  # diagnose setup
wely doctor --json           # machine-readable output

# Setup (manual)
wely init                    # wely.config.ts + package.json + autoComponents + components/
npm install

# Components
wely create w-card --props title:String --test
wely create w-user --props name:String,age:Number --actions refresh,delete
wely sync
wely list
wely docs
wely docs --watch            # regenerate on component changes
wely docs --out docs/api.md

# Build (minimal project: bundle by default; auto-discovers components)
wely build
wely build --no-auto-components
wely build --bundle
wely build --chunks         # runtime/components split when needed
wely build --all
wely build --json           # machine-readable summary
wely build --export ../app/public/vendor/wely

# Embed & integrations
wely embed                   # html-usage/index.html
wely add react
wely add vue

# Deploy
wely export ../other-project/lib
wely export ./out --no-build
wely export ../lib --clean

# Dev & test
wely dev
wely test                 # watch — vitest + jsdom (wely init adds them)
wely test --run           # single run
wely test --changed       # git-changed component tests only
wely ci                   # build + test + docs pipeline
wely ci --json

# GitHub Pages
wely page                    # docs/assets/wely.bundle.umd.js (keeps docs/index.html)

Build Output

CommandOutputUse case
wely build (no vite.config)wely.bundle.*.jsConsumer project — runtime + auto-discovered components
wely build --no-auto-componentswely.bundle.*.jsUses src/bundle.ts import chain instead of glob
wely build --chunkswely.chunked.es.js (+ optional chunks/*.js)Runtime/components split — cache-friendly
wely build (with vite.config)wely.es.js, wely.umd.jsLibrary — runtime only (packaging)

Bundle: drop-in script. Library: import { defineComponent, html } from 'welyjs'.

Chunked build: wely build --chunks separates stable runtime/core code from component code when chunking is beneficial. Use <script type="module" src="wely.chunked.es.js"></script>. If a chunks/ folder is generated, deploy it with the entry file.

Size optimization: Default uses esbuild minify. For smaller bundles, use a custom vite.config with minify: 'terser' and terserOptions: { compress: { drop_console: true } } — terser yields ~5–15% smaller output.

Styling

Tailwind CSS v4 is integrated:

Minimal setup: wely init + wely build — CLI creates tailwind.css with correct @source on first run. Bundle consumers: no config — Tailwind baked in.

Browser Access

Every mounted Wely component exposes its context on the DOM element as $wely. This enables direct access from DevTools, tests, or automation tools (e.g. MCP-based agents).

Element-level access

const el = document.querySelector('w-counter')
el.$wely.state.count        // read state
el.$wely.state.count = 10   // write state (triggers re-render)
el.$wely.actions.increment() // call an action
el.$wely.props.start         // read props
el.$wely.emit('my-event', { detail: 42 }) // dispatch event

Global helper — window.wely

Installed automatically when the runtime loads:

wely.get('w-counter')        // first matching element's ctx
wely.getAll('w-counter')     // all matching elements' ctx array
wely.list()                  // all registered tag names
wely.ready('w-counter')      // Promise: DOM ready + tag defined/upgraded
wely.whenReady(fn, 'w-counter') // callback form
MethodReturnsDescription
wely.get(selector)ComponentContext | undefinedContext of the first element matching the tag or CSS selector
wely.getAll(selector)ComponentContext[]Contexts of all matching elements
wely.list()string[]All registered component tag names
wely.ready(tag?)Promise<void>DOM parsed + custom element(s) defined — safe after defer or dynamic script load
wely.whenReady(fn, tag?)voidCallback form of ready

TypeScript: $wely is typed on HTMLElement globally. The WelyBridge interface is exported for window.wely typing.

MCP / automation: Because window.wely is plain JS, any browser automation tool (Playwright, Puppeteer, Cursor browser MCP) can call window.wely.get('w-counter').state via evaluate() to read or mutate component state programmatically.

Browser Support

BrowserMinimum
Chrome73+
Edge79+
Firefox101+
Safari16.4+

Custom Elements v1, Shadow DOM, adoptedStyleSheets, Proxy

Portability

Wely components are fully portable — build once, use anywhere. They are standard Web Components; no framework runtime at the consumer side.

AspectBehavior
OutputCustom Elements v1, Shadow DOM — no framework-specific bundle
Consumer runtimeZero Wely runtime — components are plain DOM elements
Drop-inPlain HTML, React, Vue, Angular, Svelte, Astro, Eleventy, any DOM environment
FormatsES module + UMD — bundlers or classic <script>
Deploymentwely export <path> copies output · wely embed scaffolds plain HTML usage
Integrationswely add react · wely add vue — starter snippets in integrations/

Same component, same API — works in all environments without modification.

Design Principles

Tech Stack

LayerTool
LanguageTypeScript
RenderingWely core renderer (internal)
StylingTailwind CSS v4
Dev / BuildVite
TestingVitest + jsdom
OutputES module + UMD

Project Structure

src/
  runtime/          defineComponent, config, registry, fetch, resource, store
  components/       w-counter, w-button, w-counter-card, w-pokemon-grid, w-user-list
  styles/           tailwind.css
  playground/       main.ts, app.ts, home, docs, gallery, preview-lab
index.html          Playground shell
wely.config.ts      App config
vite.config.ts      Vite + Vitest
docs/index.html     GitHub Pages single-page doc (wely page → docs/assets/wely.bundle.umd.js only)