| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Name | Name | Last commit date | ||
|---|---|---|---|---|
Important
The vendor-neutral entry point for this repo. Read it before building an app with wcstack or working on this monorepo. (Claude Code also auto-reads CLAUDE.md.)
What if the browser had these built in?
wcstack is a thought experiment turned into code. We imagine what future web standards could look like — reactive data binding, declarative routing, automatic component loading — and build them as if they already existed in the browser.
No framework. Just HTML tags that should exist.
🌐 wcstack.github.io — landing page, live demos, and the full package tour.
This project follows five strict constraints. They're what make it interesting.
| # | Rule | Why |
|---|---|---|
| 1 | Single CDN import | One <script> tag. That's it. No npm, no bundler, no config. |
| 2 | Features as custom tags | Everything is a custom element. If it can't be expressed as <wcs-something>, it doesn't belong here. |
| 3 | Initial load = tag definitions only | The script just registers custom elements. No initialization code, no bootstrap ritual. |
| 4 | Respect HTML semantics | Expressions live in data-* attributes and text nodes — places HTML already allows extension. The DOM structure and semantics stay intact. |
| 5 | Latest ECMAScript | We actively adopt cutting-edge JS features. No transpiling to ES5. This is the future, after all. |
These rules sound simple. They're not.
Respecting HTML semantics means you need to deeply understand where the spec allows extension — and where it doesn't. Building everything as custom tags means solving lifecycle, ordering, and communication within the Custom Elements spec. No dependencies means every algorithm is yours to write. And it all has to feel like it could be a browser built-in.
In every existing framework, the component is where UI meets state. Even with external stores, you still write glue code inside the component to pull state in. State and UI always couple through JavaScript.
wcstack takes a different path. Literally.
The only contract between UI and state is a path string — user.name, cart.items.*.subtotal, @shared. No hooks. No imports. No glue code. The component's JavaScript doesn't contain a single line that references state. The HTML alone describes every data dependency.
State ← "user.name" → UI Path binds the two layers Comp A ← "@app" → Comp B Named path crosses components Loop ← "items.*" → Template Wildcard abstracts the index
This means you can redesign the UI without touching state, refactor state without touching the DOM, and read the HTML to understand everything. It's the same idea as a REST URL — a simple string contract, no shared code.
Working with an AI coding agent — Claude Code, Codex, Cursor, Copilot, or anything else? Point it at AGENTS.md first. It is the vendor-neutral entry point for this repository, and it covers both directions:
Claude Code reads CLAUDE.md (the more detailed, tool-specific guide) automatically; tell every other agent to start at AGENTS.md.
Forty-five independent runtime packages + one tooling extension package. Zero runtime dependencies (except happy-dom for SSR). No build step required.
@wcstack/state — Declare state inline, bind it to the DOM with attributes.
<wcs-state>
<script type="module">
export default {
taxRate: 0.1,
cart: {
items: [
{ name: "Widget", price: 500, quantity: 2 },
{ name: "Gadget", price: 1200, quantity: 1 }
]
},
removeItem(event, index) {
this["cart.items"] = this["cart.items"].toSpliced(index, 1);
},
get "cart.items.*.subtotal"() {
return this["cart.items.*.price"] * this["cart.items.*.quantity"];
},
get "cart.total"() {
return this.$getAll("cart.items.*.subtotal", []).reduce((a, b) => a + b, 0);
},
get "cart.grandTotal"() {
return this["cart.total"] * (1 + this.taxRate);
}
};
</script>
</wcs-state>
<template data-wcs="for: cart.items">
<div>
{{ .name }} ×
<input type="number" data-wcs="value: .quantity">
= <span data-wcs="textContent: .subtotal|locale"></span>
<button data-wcs="onclick: removeItem">Delete</button>
</div>
</template>
<p>Grand Total: <span data-wcs="textContent: cart.grandTotal|locale(ja-JP)"></span></p>@wcstack/router — Define your app's navigation structure in markup.
<wcs-router>
<template>
<wcs-route path="/">
<wcs-layout layout="main-layout">
<nav slot="header">
<wcs-link to="/">Home</wcs-link>
<wcs-link to="/products">Products</wcs-link>
</nav>
<wcs-route index>
<wcs-head><title>Home</title></wcs-head>
<app-home></app-home>
</wcs-route>
<wcs-route path="products">
<wcs-route index>
<product-list></product-list>
</wcs-route>
<wcs-route path=":id(int)">
<product-detail data-bind="props"></product-detail>
</wcs-route>
</wcs-route>
</wcs-layout>
</wcs-route>
<wcs-route fallback>
<error-404></error-404>
</wcs-route>
</template>
</wcs-router>
<wcs-outlet></wcs-outlet>@wcstack/fetch — Declarative HTTP communication as a headless Web Component.
<wcs-state>
<script type="module">
export default {
users: [],
loading: false,
filterRole: "",
get usersUrl() {
const role = this.filterRole;
return role ? "/api/users?role=" + role : "/api/users";
},
};
</script>
</wcs-state>
<!-- URL changes automatically trigger re-fetch -->
<wcs-fetch data-wcs="url: usersUrl; value: users; loading: loading"></wcs-fetch>
<template data-wcs="if: loading">
<p>Loading...</p>
</template>
<template data-wcs="for: users">
<div data-wcs="textContent: .name"></div>
</template>@wcstack/autoloader — Write a tag, it loads. No registration needed.
<script type="importmap">
{
"imports": {
"@components/ui/": "./components/ui/",
"@components/ui|lit/": "./components/ui-lit/"
}
}
</script>
<!-- Auto-loaded from ./components/ui/button.js -->
<ui-button></ui-button>
<!-- Auto-loaded with Lit loader from ./components/ui-lit/card.js -->
<ui-lit-card></ui-lit-card>@wcstack/server — Same HTML, server-rendered. No special syntax needed.
import { renderToString } from "@wcstack/server";
const html = await renderToString(`
<wcs-state enable-ssr>
<script type="module">
export default {
items: [],
async $connectedCallback() {
const res = await fetch("/api/items");
this.items = await res.json();
}
};
</script>
</wcs-state>
<template data-wcs="for: items">
<div data-wcs="textContent: items.*.name"></div>
</template>
`);<!DOCTYPE html>
<html>
<head>
<script type="module" src="https://esm.run/@wcstack/state/auto"></script>
</head>
<body>
<wcs-state>
<script type="module">
export default {
count: 0,
countUp() { this.count++; }
};
</script>
</wcs-state>
<p>Count: {{ count }}</p>
<button data-wcs="onclick: countUp">+1</button>
</body>
</html>One <script> tag. One custom element. Pure HTML. That's it.
Enforcing a Content-Security-Policy? The inline <script type="module"> above is evaluated through a blob: URL and needs script-src blob:; moving the state into src="./state.js" needs no extra directive. Per-feature directive table: docs/csp.md.
For production, pin the version and add an integrity attribute. dist/auto.min.js is a self-contained bundle with zero imports, so one hash covers every line of wcstack that runs — the usual ESM caveat, where integrity protects only the entry and not what it imports, does not apply:
<script type="module"
src="https://cdn.jsdelivr.net/npm/@wcstack/state@1.26.0/dist/auto.min.js"
integrity="sha384-..."></script>Digests for every package ship in each GitHub Release (and as an attached sri.json), computed from the published tree rather than read back from the CDN. Details, and what the hash deliberately does not cover: docs/sri.md.
Every I/O node reflects its boolean output states (loading, connected, error, granted, …) into CustomStateSet, so plain CSS can react to component state — no JavaScript required:
wcs-fetch:state(loading) ~ .spinner { display: block; }
form:has(wcs-fetch:state(error)) .msg { display: block; }
wcs-ws:state(connected) ~ .indicator { color: limegreen; }
wcs-permission:state(denied) ~ .help { display: block; }Each package README lists its reflected states. Supported in Chrome/Edge 125+, Safari 17.4+, Firefox 126+; in older browsers the styles simply don't apply — the components keep working. States are not serialized into SSR output (combine with wcs-x:not(:defined) for first-paint styling).
For debugging, add the debug-states attribute to a tag to mirror its states as data-wcs-state-* attributes in the DevTools Elements panel, or read the debugStates property. Write production CSS against :state(), not those attributes.
Every I/O node implements wc-bindable-protocol, so a thin adapter (@wc-bindable/react, /vue, /svelte, /solid, …) wires an element's outputs into framework state without per-element glue. Three rules make that work:
1. Import the definition before you render. Adapters check isWcBindable(el) once on mount and never retry, so an element that upgrades later stays silently unbound. A static import at the app entry is the reliable fix:
import "@wcstack/websocket/auto"; // main.tsx / main.js — before the app rendersIf the definition genuinely has to arrive late (autoloader, CDN tag, code-split), gate the mount on customElements.whenDefined("wcs-ws"). connectedCallbackPromise is not a substitute — it covers connection, not definition.
2. Pass object-valued inputs as properties. DOM attributes only hold strings, and several frameworks fall back to attributes when the property is not on the element yet, which stringifies your payload. Use .prop (Vue), prop: (Solid), .prop= (Lit), or assign through a ref.
3. Unwrap reactive proxies before handing values in. Vue's reactive, Svelte's $state and Qwik's useStore wrap plain objects in proxies, and a proxy cannot cross a structured-clone boundary — <wcs-worker> and <wcs-broadcast> will report a DataCloneError instead of sending. Pass toRaw() / $state.snapshot() / unwrap() results.
Live handles such as <wcs-camera>'s MediaStream are deliberately not snapshot state: take them from the element event via a ref. Note that Angular templates and JSX cannot bind event names containing a colon, so addEventListener (or Renderer2.listen) is the portable path.
Full guide, per-framework snippets and the reasoning: docs/framework-adapter-integration.md. Working demos: examples/websocket-chat (React 19 and Vue 3 against the same server as the vanilla, state and signals variants).
wcstack/ ├── packages/ │ ├── state/ # @wcstack/state │ ├── router/ # @wcstack/router │ ├── fetch/ # @wcstack/fetch │ ├── autoloader/ # @wcstack/autoloader │ ├── server/ # @wcstack/server │ ├── storage/ # @wcstack/storage │ ├── timer/ # @wcstack/timer │ ├── raf/ # @wcstack/raf │ ├── geolocation/ # @wcstack/geolocation │ ├── websocket/ # @wcstack/websocket │ ├── upload/ # @wcstack/upload │ ├── debounce/ # @wcstack/debounce │ ├── clipboard/ # @wcstack/clipboard │ ├── broadcast/ # @wcstack/broadcast │ ├── worker/ # @wcstack/worker │ ├── sse/ # @wcstack/sse │ ├── intersection/ # @wcstack/intersection │ ├── wakelock/ # @wcstack/wakelock │ ├── resize/ # @wcstack/resize │ ├── speech/ # @wcstack/speech │ ├── permission/ # @wcstack/permission │ ├── network/ # @wcstack/network │ ├── screen-orientation/ # @wcstack/screen-orientation │ ├── fullscreen/ # @wcstack/fullscreen │ ├── picture-in-picture/ # @wcstack/picture-in-picture │ ├── pointer-lock/ # @wcstack/pointer-lock │ ├── share/ # @wcstack/share │ ├── eyedropper/ # @wcstack/eyedropper │ ├── contacts/ # @wcstack/contacts │ ├── credential/ # @wcstack/credential │ ├── idle/ # @wcstack/idle │ ├── tilt/ # @wcstack/tilt │ ├── accelerometer/ # @wcstack/accelerometer │ ├── gyroscope/ # @wcstack/gyroscope │ ├── magnetometer/ # @wcstack/magnetometer │ ├── ambient-light-sensor/ # @wcstack/ambient-light-sensor │ ├── notification/ # @wcstack/notification │ ├── defined/ # @wcstack/defined │ ├── camera/ # @wcstack/camera │ ├── audio/ # @wcstack/audio │ ├── midi/ # @wcstack/midi │ ├── view-transition/ # @wcstack/view-transition │ ├── signals/ # @wcstack/signals │ ├── devtools/ # @wcstack/devtools │ ├── lint/ # @wcstack/lint │ └── vscode-wcs/ # wcstack-intellisense (VS Code extension)
Each package is independently built, tested, and published.
Examples under examples/ track the packages that still live in this repository. The former AI/Auth0 demos moved to @csbc-dev/ai-agent and @csbc-dev/auth0, and are no longer included here. The legacy npm packages @wcstack/ai and @wcstack/auth0 are deprecated.
Commands run from within a specific package directory (e.g., packages/state/):
npm run build # Clean dist, compile TypeScript, bundle with Rollup
npm test # Run tests (Vitest)
npm run test:coverage # Coverage (100% statements/functions/lines, 97%+ branches)
npm run lint # ESLintMIT
| Back | FazBrowse Home | New Git URL |