Latest release: v4.1.1

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, and thirty-plus Web APIs as plain HTML tags — and build them as if they already existed in the browser.

未来のWeb標準を想像し、それがすでにブラウザに存在するかのようにコードにする思考実験。30以上のWeb APIをHTMLタグに。

47
Packages
30+
Web API Tags
0
Dependencies
1
Script Tag Each

Shipped in v4.0.0 → v4.1.1

4.0 replaces the engine inside @wcstack/state — a smaller core and faster updates behind the same markup: auto.min.js goes from 88.0 to 47.7 KB gzip, and the CPU-weighted geometric mean on the official js-framework-benchmark is 1.07, against 1.44 for 3.5.4. Most of what breaks is what 3.5 announced, and there is no compatibility layer: upgrade to 3.5, clear its warnings, then move every @wcstack/* package to 4.0 at once — @wcstack/server together with the client — following the migration table and the 3.x → 4.0 migration guide. The VS Code extension ships the 4.0 rules as 2.0.0; keep 1.21.x, @wcstack/lint 3.5 and @wcstack/typescript 3.5 for 3.x projects. v4.0.1 comes from writing the tutorial: a class: or attr: without a name is refused by name (#122), and the lint stops reporting the fields of a list a getter returns and reports an unknown filter as an error. v4.0.2 comes from Part 2: the lint learns the shape of data loaded from outside from a JSDoc type, and reports a <wcs-storage> binding that would not save (wcs/input-nested-write, wcs/storage-init-seed). v4.0.3 comes from Part 3: the lint also catches a storage binding that forgets #init=element and a <wcs-fetch> body sent only once, and checks pinned URLs. v4.0.4, also from Part 3: a <wcs-outlet> placed away from its <wcs-router> stayed empty without a word — the router now warns about it, and so does the lint. v4.1.0 comes from four seam apps — apps written from the documents alone to find what tests miss: route guards that redirect without trapping Back, a confirmation before leaving unsaved changes (leaveConfirm), <wcs-network offline>, view transitions only for list and branch changes, and keyed signals rows that see their current item. v4.1.1 puts those apps in the repository's examples, each with a real-browser test, and fixes what that found: the lint now knows <wcs-network offline>. The numbers are in Performance, the rest in the version notes of the state page.

4.0は@wcstack/stateの中のエンジンを置き換える — 同じマークアップの裏で、より小さなコアとより速い更新を。auto.min.jsは88.0から47.7 KB gzipへ、公式のjs-framework-benchmarkでのCPUの重み付き幾何平均は1.07(3.5.4は1.44)だ。変わるものの大半は3.5が予告したもので、互換層は無い: 3.5に上げ、その警告を消し、それから@wcstack/*のパッケージをすべて一度に4.0へ移す — @wcstack/serverもクライアントと一緒に — 手順は移行表と3.x → 4.0移行ガイドにある。VS Code拡張は4.0の規則を2.0.0として出す。3.xのプロジェクトでは1.21.x、@wcstack/lint 3.5、@wcstack/typescript 3.5を使い続けること。v4.0.1はチュートリアルを書く中で見つかったものを直す: 名前の無いclass:やattr:は名指しで拒まれ(#122)、lintはgetterが返すリストのフィールドを報告しなくなり、未知のフィルタをerrorとして報告する。v4.0.2はPart 2から: lintは外から読むデータの形をJSDocの型で知り、保存されない<wcs-storage>のバインディングを報告する(wcs/input-nested-write・wcs/storage-init-seed)。v4.0.3はPart 3から: lintは#init=elementを忘れたstorageのバインディングと、1回しか送られない<wcs-fetch>の本文も見つけ、固定したURLも検査する。v4.0.4もPart 3から: <wcs-router>から離れた<wcs-outlet>は何も言わずに空のままだった — いまはrouterが警告し、lintも報告する。v4.1.0は、テストが見逃すものを見つけるために文書だけから書いた4本の継ぎ目アプリから: 「戻る」を閉じ込めないガードのリダイレクト、未保存の変更を残して出る前の確認(leaveConfirm)、<wcs-network offline>、一覧と分岐の変化だけのビュー遷移、今のitemを見るsignalsのkey付きの行。v4.1.1はそのアプリを実ブラウザのテスト付きでリポジトリのexamplesに置き、そこで見つかったものを直す: lintが<wcs-network offline>を知るようになった。数値はPerformanceに、残りはstateのページの版ごとの注記にある。

v4.0.0 <wcs-state>

A new engine behind the same markup

What if the engine were rewritten from scratch — and no page had to change a tag?

4.0 keeps everything you write against and replaces what runs it. Built with esbuild and terser, auto.min.js is 47.7 KB gzip (88.0 in 3.5.4); on the official js-framework-benchmark the CPU-weighted geometric mean is 1.07 against 1.44, with about a third of the JavaScript time and 1,000 rows in 2.97 MB instead of 6.35. A list whose items repeat no longer goes quadratic — a 90,000-cell board from 4.6 s to 6 ms — and the new engine closes issues open against 3.x.

4.0は、書く側が向き合うものをすべて残し、それを動かすものを取り替える。esbuildとterserでビルドし、auto.min.jsは47.7 KB gzip(3.5.4では88.0)、公式のjs-framework-benchmarkでのCPUの重み付き幾何平均は1.07(3.5.4は1.44)で、JavaScriptの時間は約3分の1、1,000行のメモリは6.35 MBから2.97 MBになった。要素が繰り返すリストはもう二乗にならず — 9万セルの盤面は4.6 sから6 ms — 新しいエンジンは3.xに対して開いていたissueも閉じる。

  • Same Markup, New Engine
  • auto.min.js 47.7 KB (−46 %)
  • CPU 1.07 vs 1.44
  • 90,000 Cells: 4.6 s → 6 ms
Learn more →
v4.0.0 $behavior: { enableMustache: false }

What 3.5 announced, done

What if every warning of the last minor turned into exactly one error, named where you wrote it?

4.0 removes what 3.5 named with [wcs/v4-migration]: $scan, the names 3.2 renamed, the substr filter and three bootstrapState() options. A removed filter name fails with [wcs/filter-unknown] naming its replacement. enableMustache, sameValueGuard and enableDirectionalInitialSync move to the $behavior of each root state, every package’s bootstrapXxx(config) throws on an option it does not have, and failures are reported once (#49, #50).

4.0は3.5が[wcs/v4-migration]で名指ししたもの — $scan、3.2が改名した旧名、substrフィルタ、bootstrapState()の3つのオプション — を取り除く。取り除かれたフィルタ名は、置き換え先を名指しする[wcs/filter-unknown]で失敗する。enableMustache・sameValueGuard・enableDirectionalInitialSyncは各ルートのstateの$behaviorへ移り、各パッケージのbootstrapXxx(config)は、持っていないオプションでthrowする。失敗は一度だけ報告される(#49、#50)。

  • $scan and the 3.2 Names Removed
  • $behavior per Root State
  • Every bootstrapXxx() Throws
  • Failures Reported Once
Learn more →
v4.0.0 onclick#direct,stop: save

One listener per event type

What if a thousand rows of buttons needed one click listener, not a thousand?

on*: bindings of eleven common event types — click, input, change, submit, keydown, … — are delegated: one listener per type sits on the root the state binds and runs the handlers of the elements the event passed, innermost first. event.currentTarget is that root, so find the element with event.target.closest(…) or use the loop index, and an ancestor that calls stopPropagation() keeps the handlers inside it from running. on*#direct: keeps the 3.x listener on the element for one binding.

よく使う11種類のイベント — click・input・change・submit・keydownなど — のon*:バインディングは委譲される: 種類ごとに1つのリスナーがstateのバインドするルートに置かれ、イベントが通った要素のハンドラを内側から順に走らせる。event.currentTargetはそのルートなので、要素はevent.target.closest(…)で探すかループのインデックスを使う。stopPropagation()を呼ぶ祖先は、その内側のハンドラを走らせない。on*#direct:はバインディング1つについて、3.xと同じく要素にリスナーを付ける。

  • 11 Event Types Delegated
  • currentTarget Is the Root
  • #direct Opts One Binding Out
  • Two-Way Stays on the Element
Learn more →
v4.0.0 this["items.0"] = item

A row stays where it is

What if writing a list element changed what that row shows — and nothing moved?

Writing a list element (this["items.0"] = …) replaces the value at that position: the row stays where it is, with its focus and other DOM state, and everything derived below it is computed again. To move rows, assign a new array — rows follow their objects by identity. for: takes no filters (#121): loop over a getter that returns the list, and writes through its rows reach the source (#365). A numeric segment is an index only under an array: usersById.42.name is a key (#382).

リストの要素への書き込み(this["items.0"] = …)はその位置の値を置き換える: 行はその場にとどまり、フォーカスなどのDOMの状態もそのままで、その下で導かれるものは計算し直される。行を動かすには新しい配列を代入する — 行は同一性でオブジェクトについていく。for:はフィルタを取らない(#121): リストを返すgetterの上で回せば、その行を通した書き込みは元のリストに届く(#365)。数値の区間が添字になるのは配列の下だけで、usersById.42.nameはキーだ(#382)。

  • Element Writes Replace in Place
  • Reorder by a New Array
  • for: Over a Getter, Not a Filter
  • Writes Reach Filtered Copies
Learn more →
v4.0.0 "items.*.price"(cur, prev, i)

Watches that need no for:

What if a row watch fired for every row that changed — rendered or not?

A row watch is headless now: $watch: { "items.*.price"(cur, prev, i) { … } } fires for a list the page does not render, where 3.x fired it zero times. A watch on a row getter is evaluated eagerly for every row, with a real prev, so keep it cheap. Assigning a whole array fires only the rows that came in (#389), and a $watch → $stream loop stops after about 16 laps instead of freezing the page.

行のwatchはいまヘッドレスだ: $watch: { "items.*.price"(cur, prev, i) { … } }は、ページが描いていないリストでも発火する(3.xでは一度も発火しなかった)。行getterへのwatchは全行について先行して評価され、prevも本当の前の値になるので、軽く保つこと。配列全体の代入が発火させるのは新しく入った行だけで(#389)、$watch → $streamのループはページを止めずに約16周で打ち切られる。

  • Row Watches Without for:
  • Row Getters Eager, prev Real
  • Whole Arrays Fire New Rows
  • Stream Loops Cut at ~16 Laps
Learn more →
v4.0.0 onclick: cart.checkout

Volumes hold data, components run their own

What if a module held only its data — and a component ran everything it declares?

A volume (<wcs-state mount="cart">) holds data, getters, setters and methods, and its methods are reachable by path — onclick: cart.checkout. One that declares $watch, $listKeys, $renderedCallback, $behavior or $features, or carries a data-wcs, is not grafted: declare those on the root with full paths. A mounted component goes the other way: it runs its own $listKeys, tokens, $on and $errorCallback, and a host inserted by code is wired when its <wcs-state bind-component> connects.

ボリューム(<wcs-state mount="cart">)が持つのはデータ・getter・setter・メソッドで、メソッドはパスで届く — onclick: cart.checkout。$watch・$listKeys・$renderedCallback・$behavior・$featuresを宣言したボリュームや、data-wcsを持つボリュームは接ぎ木されない: それらはルートに完全なパスで書く。マウントされたコンポーネントは逆へ向かう: 自分の$listKeys・トークン・$on・$errorCallbackを動かし、コードが差し込んだホストは、その<wcs-state bind-component>が接続したときに配線される。

  • Volumes: Data, Getters, Methods
  • Volume Methods by Path
  • Components Run Their Own Tokens
  • Unbound Hosts Wired on Connect
Learn more →
v4.0.0 <wcs-ssr version="4.0.0">

Server and client move together

What if a server-rendered page hydrated every row it drew — nested ones too?

4.0 writes a new server output — rows, branches and text bindings marked with comments, no data-wcs-ssr-id or props table — and the client adopts the server’s rows and branches where they are, nested ones included. The output is not compatible across 3.x and 4.0: a 4.0 client discards a 3.x snapshot with one warning and renders on the client, and a 4.0 server with a 3.x client is not supported. Deploy @wcstack/server 4.0 with the 4.0 client, purge HTML cached from 3.x, and keep the comments (removeComments breaks hydration).

4.0は新しいサーバー出力を書く — 行・枝・テキストバインディングはコメントで印が付き、data-wcs-ssr-idもプロパティの表も無い — そしてクライアントはサーバーの行と枝を、入れ子のものも含めて、その場で引き取る。出力は3.xと4.0の間で互換が無い: 4.0のクライアントは3.xのスナップショットを警告1つとともに捨ててクライアントで描き、4.0のサーバーと3.xのクライアントの組み合わせはサポートされない。@wcstack/server 4.0を4.0のクライアントと一緒にデプロイし、3.xでキャッシュしたHTMLを捨て、コメントは残すこと(removeCommentsはハイドレーションを壊す)。

  • Nested Rows Hydrate
  • No Props Table
  • Form Values in the Markup
  • 3.x Snapshots Rendered Again
Learn more →
v4.0.0 command.showModal: $command.edit

Native elements take commands

What if a state opened a dialog the way it starts a fetch — by emitting a token?

With the native-commands feature, which @wcstack/state and /auto include, command.<method>: subscribes a native element’s method from a fixed table — showModal / close on <dialog>, focus, requestSubmit, play / pause, … — so the state opens a dialog without ever holding the element; 3.x refused native elements. The split build gains one-tag loading: dist/split/auto.js loads the core and the features the page names in features="…" on the root and $features in each state.

@wcstack/stateと/autoが載せているnative-commands機能があれば、command.<method>:は決まった表にあるネイティブ要素のメソッドを購読する — <dialog>のshowModal / close、focus、requestSubmit、play / pauseなど — だからstateは要素を一度も持たずにダイアログを開く。3.xはネイティブ要素を拒んでいた。分割形のビルドはタグ1つでの読み込みを得る: dist/split/auto.jsが、コアと、ルートのfeatures="…"や各stateの$featuresで名指しした機能を読み込む。

  • command.showModal: on a Dialog
  • A Fixed Table, Event Args Dropped
  • Split Build From One Tag
  • $features per State
Learn more →
v4.0.0 <wcs-wakelock data-wcs="active: awake">

An input is a property

What if a false written to a tag stayed false — as the element understands it?

State writes a custom element’s wc-bindable input to its property only; reflecting it to the attribute is the element’s job in its setter. 3.x’s copy turned a false into "false", which switched on <wcs-wakelock active> and every manual — an element of your own that relied on state writing its attribute needs a setter that reflects it. The I/O nodes handle null (removes the attribute) and undefined (restores the initial one) instead of opening "undefined", and the router, <wcs-defined>, <wcs-throttle>, <wcs-wakelock>, <wcs-ws> and DevTools bring fixes.

stateはカスタム要素のwc-bindableの入力をプロパティにだけ書く。属性への反映はsetterの中で要素がすることだ。3.xの写しはfalseを"false"にして、<wcs-wakelock active>やすべてのmanualをオンにしていた — stateが属性を書くことに頼っていた自作の要素には、それを反映するsetterが要る。I/Oノードはnull(属性を取り除く)とundefined(最初の属性を戻す)を扱い、"undefined"を開くことはもう無い。ルーター・<wcs-defined>・<wcs-throttle>・<wcs-wakelock>・<wcs-ws>・DevToolsにも修正が入る。

  • Property Only, No Attribute Mirror
  • null Clears, undefined Restores
  • bindInput Follows A2
  • Router, Throttle, Wakelock, ws Fixed
Learn more →

The only contract is a path string

No hooks. No imports. No glue code.

フックなし。インポートなし。グルーコードなし。

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, cart.total. The component's JavaScript doesn't contain a single line that references state. The HTML alone describes every data dependency — the same idea as a REST URL: a simple string contract, no shared code.

State   ← "user.name" →  UI          Path binds the two layers
Comp A  ← "cart.total" → Comp B      Mounted path crosses components
Loop    ← "items.*" →    Template    Wildcard abstracts the index

UIと状態を結ぶ唯一の契約はパス文字列。UIを作り直しても状態に触れなくていい。状態をリファクタリングしてもDOMに触れなくていい。HTMLを読めばすべてが分かる。

HTML tags that should exist

What's missing from the browser — and what we built.

ブラウザに足りないもの — そして我々が作ったもの。

<wcs-state>

Reactive State

What if HTML had built-in reactive data binding?

Reactive state with Mustache syntax, path getters for computed properties, 40+ built-in filters, two-way binding, and structural directives — no virtual DOM.

Mustache構文、パスゲッター、40以上のビルトインフィルタ、双方向バインディング、構造ディレクティブ対応のリアクティブ状態管理。

  • Mustache Syntax
  • Path Getters
  • 40+ Filters
  • Two-way Binding
  • for / if / else
Learn more →
<wcs-router>

Declarative Router

What if you could define SPA routes directly in HTML?

Define routes with nested layouts, typed URL parameters (:id(int)), route guards, and per-route <head> management. Built on the Navigation API.

ネストレイアウト、型付きURLパラメータ、ルートガード、ルート別head管理を備えた宣言的ルーティング。

  • Nested Routes
  • Typed Params
  • Layouts
  • Route Guards
  • Head Management
<wcs-autoloader>

Component Autoloader

What if the browser could auto-import components just by seeing their tags?

Detects custom elements in the DOM and dynamically imports them via Import Maps. Supports eager & lazy loading with MutationObserver for dynamically added elements.

DOMのカスタム要素を検出し、Import Mapsで動的インポート。Eager/Lazy読み込みとMutationObserverに対応。

  • Import Maps
  • Eager / Lazy
  • MutationObserver
  • Pluggable Loaders
@wcstack/signals

Signals Core

What if TC39 signals were already here — and drove the same tags?

A fine-grained reactive core — signal / computed / effect, async resources, keyed For / Index — the JS-first counterpart to <wcs-state>, binding the same elements or their headless Cores.

signal / computed / effect、非同期resource、キー付きFor/Indexを備えたシグナルベースのリアクティブコア。同じ要素やヘッドレスCoreを束縛できるstateのJSファースト版。

  • TC39-shaped
  • Async Resources
  • Keyed For / Index
  • bindNode / mountNode
Learn more →
@wcstack/server

Server-Side Rendering

What if your templates rendered on the server — unchanged?

Same HTML, server-rendered. Add enable-ssr, call renderToString(), done. Automatic hydration with zero flicker, and version-safe fallback to CSR.

同じHTMLをサーバーでレンダリング。enable-ssrを付けてrenderToString()を呼ぶだけ。ゼロフリッカーの自動ハイドレーション付き。

  • Drop-in SSR
  • Auto Hydration
  • Zero Flicker
  • CSR Fallback
<wcs-fetch>

Fetch as a Tag

What if fetch was a tag?

Declarative HTTP as a headless component. Bind a URL, get data — URL changes automatically re-fetch. htmx-like HTML replace mode, and a headless core that runs in Node.js, Deno, and Workers.

宣言的HTTP通信。URLをバインドすればデータが届く。URL変更で自動再フェッチ。htmxライクなHTML置換モードも。

  • URL Observation
  • Trigger Property
  • HTML Replace
  • Headless Core

What if every Web API was a tag?

Thirty-plus declarative wrappers over the Web platform — each one a headless custom element that binds straight into your state.

Webプラットフォームの30以上のAPIを宣言的タグ化。すべてヘッドレスなカスタム要素として、状態に直接バインドされる。

<wcs-fetch>HTTP requests, auto re-fetch
<wcs-storage>localStorage / sessionStorage sync
<wcs-upload>File upload with progress
<wcs-ws>WebSocket real-time comms
<wcs-sse>Server-Sent Events streaming
<wcs-broadcast>Cross-tab messaging
<wcs-worker>Web Worker offloading
<wcs-timer>Ticks, elapsed time, polling
<wcs-raf>Frame ticks with dt, tab-aware
<wcs-debounce> <wcs-throttle>Value & signal coalescing
<wcs-clipboard>Clipboard read / write / monitor
<wcs-geo>Geolocation, live permission
<wcs-permission>Permissions API monitor
<wcs-notify>Desktop notifications
<wcs-intersect>Visibility, lazy-load, scrollspy
<wcs-resize>Element size observation
<wcs-wakelock>Screen Wake Lock
<wcs-camera> <wcs-recorder>Camera capture & recording
<wcs-speak> <wcs-listen>Speech synthesis & recognition
<wcs-defined>Custom-element readiness gate
<wcs-fullscreen>Fullscreen API
<wcs-pip>Picture-in-Picture
<wcs-pointer-lock>Pointer Lock for games / canvas
<wcs-screen-orientation>Orientation monitor & lock
<wcs-idle>Idle Detection
<wcs-network>Network Information monitor
<wcs-media-query>matchMedia: dark mode, reduced motion, breakpoints
<wcs-share>Web Share sheet
<wcs-contacts>Contact Picker
<wcs-credential>Credential Management
<wcs-eyedropper>EyeDropper color picker
<wcs-tilt>Device Orientation, iOS-safe
<wcs-accelerometer> <wcs-gyroscope>Motion sensors (x / y / z)
<wcs-magnetometer> <wcs-ambient-light-sensor>Magnetic field & illuminance
<wcs-audio> <wcs-osc> <wcs-gain>Web Audio graph as markup (11 tags)
<wcs-midi>Web MIDI in & out, one tag
<wcs-view-transition>Leave & move animations, policy only

One protocol behind them all: wc-bindable

Every I/O node exposes a static manifest — observable properties, settable inputs, invocable commands — so any binding core can discover and wire it generically. Since v1.24.0 each observable also declares its semantics — state, event, or handle — so a generic adapter can tell a current value from an occurrence. <wcs-state> and @wcstack/signals speak it natively; thin adapters connect React, Vue, Svelte, and Solid.

すべてのI/Oノードは静的マニフェスト(properties / inputs / commands)を公開。v1.24.0からは各observableがsemantics(state / event / handle)も宣言し、汎用アダプタが現在値と発生を区別できる。stateとsignalsはネイティブ対応、薄いアダプタでReact / Vue / Svelte / Solidからも使える。

Code that reads like HTML

Because it is HTML.

HTMLのように読める。なぜなら、HTMLだから。

index.html State
<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 }} &times;
    <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>

<!-- Filters transform VALUES only. An event handler never takes a filter.
     BAD:  data-wcs="onkeydown: addTodo|enter"   <- no such filter exists
     GOOD: data-wcs="onkeydown: addTodo"         <- inspect event.key in the method
     Mutating state in place is not reactive either.
     BAD:  this.cart.items.push(x)
     GOOD: this["cart.items"] = [...this["cart.items"], x] -->
index.html Router
<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 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>
index.html Fetch
<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>
index.html Streams
<wcs-state>
  <script type="module">
    export default {
      prompt: "",
      // Fold an async producer into one reactive property.
      $stream: {
        tokens: {
          args:    (state) => state.prompt,            // dependency captured here
          source:  (prompt, signal) => llmStream(prompt, signal),
          fold:    (acc, chunk) => acc + chunk,        // accumulate
          initial: "",
        },
      },
    };
  </script>
</wcs-state>

<!-- Change prompt → the run aborts and restarts (switchMap) -->
<input data-wcs="value: prompt">
<p data-wcs="textContent: tokens"></p>
<p data-wcs="textContent: $streamStatus.tokens"></p>
<p data-wcs="textContent: $streamError.tokens"></p>
server.js SSR
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>
`);
// Client hydrates automatically — zero flicker.
index.html Autoloader
<!-- Define your import maps -->
<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 -->
<ui-lit-card></ui-lit-card>

Rules of the Game

This project follows five strict constraints. They're what make it interesting.

5つの厳格な制約。これが面白さの源泉。

1

Single CDN import

One <script> tag. That's it. No npm, no bundler, no config.

scriptタグ1つ。npm不要、バンドラー不要、設定不要。

2

Features as custom tags

Everything is a custom element. If it can't be expressed as <wcs-something>, it doesn't belong here.

すべてはカスタム要素。<wcs-something>で表現できなければ、ここには属さない。

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.

式はdata-*属性とテキストノードに配置 — HTMLが拡張を許可する場所のみ。DOM構造とセマンティクスはそのまま。

5

Latest ECMAScript

We actively adopt cutting-edge JS features. No transpiling to ES5. This is the future, after all.

最新のJS機能を積極採用。ES5へのトランスパイルなし。未来のプロジェクトだから。

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.

シンプルに聞こえるルールだが、実際は違う。HTMLセマンティクスを尊重するなら、仕様が拡張を許す場所と許さない場所を深く理解する必要がある。すべてをカスタムタグで構築するなら、Custom Elementsの仕様の中でライフサイクル・順序・通信を解決しなければならない。依存ゼロなら、あらゆるアルゴリズムを自分で書く。そしてそのすべてが「ブラウザ組み込みかもしれない」と感じさせなければならない。

One package, one script tag.

Pick only the scoped packages you need. Element packages ship an /auto entry.

必要なスコープ付きパッケージだけを選ぶ。要素パッケージは/autoエントリを持つ。

index.html CDN via esm.run
<!-- Load the individual @wcstack/* packages your app uses -->
<script type="module" src="https://esm.run/@wcstack/router/auto"></script>
<script type="module" src="https://esm.run/@wcstack/fetch/auto"></script>
<script type="module" src="https://esm.run/@wcstack/autoloader/auto"></script>

<!-- Dev builds only: in-page DevTools overlay -->
<script type="module" src="https://esm.run/@wcstack/devtools/auto"></script>

<!-- State last: command-token emissions are not replayed -->
<script type="module" src="https://esm.run/@wcstack/state/auto"></script>

<!-- Or (v1.32.0+): the SPA core in ONE request — state + router + fetch +
     storage + autoloader pre-linked, one SRI hash covering all of it -->
<script type="module" src="https://esm.run/wcstack/auto"></script>
terminal Verify what you wrote
# Check any HTML against the data-wcs contract. No install, no config.
# Exit code 0 means clean — iterate until it exits 0.
npx @wcstack/lint index.html

# Print the full authoring guide as plain text:
# binding syntax, filters, tokens, and the mistakes to avoid.
npm view wcstack readme

Each script registers its custom elements and does nothing else. No initialization, no bootstrap — tags activate when the browser parses them.

各スクリプトはカスタム要素を登録するだけ。初期化もブートストラップもなし — ブラウザがパースした時にタグが起動する。

New to wcstack? The tutorial builds a small shop — catalog, cart, totals, search — in one HTML file, in seven steps, loads the catalog from a file and keeps the cart in five more, and grows it into a tested, published app — pages, a component, tests — in four more.

はじめてならチュートリアルへ。商品一覧・カート・合計・検索のある小さなショップを、HTMLファイル1つで7ステップで作り、さらに5ステップで商品一覧をファイルから読んでカートを残し、もう4ステップでページ・部品・テストのある、公開したアプリに育てる。

Editor support: the wcstack-intellisense VS Code extension brings TypeScript language features — completion, diagnostics, hover — to <wcs-state> inline scripts in your HTML.

VS Code拡張「wcstack-intellisense」が、HTML内の<wcs-state>インラインスクリプトに補完・診断・ホバーを提供。