Latest release: v1.32.0

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タグに。

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

Shipped in v1.32.0

The SPA release: server-rendered routes the client adopts, markup that binds whenever it arrives, animations as policy, and a router that speaks its whole state.

SPAのためのリリース。サーバーが描いたルートをクライアントが引き継ぎ、いつ届いたマークアップでもバインドされ、アニメーションはポリシーになり、ルーターは自分の全状態を語る。

v1.32.0 <wcs-router enable-ssr>

The server renders, the client adopts

What if SSR meant adopting the DOM, not re-rendering it?

Put enable-ssr on the router and pass the request URL to @wcstack/server’s renderToString(html, { url }): the initial route is rendered server-side — typed params, nested routes, and for: / if: templates inside route content included — and the client-side router adopts that DOM on boot instead of re-rendering it, with state bindings hydrated live on the same nodes. Anything that fails verification (URL mismatch, edited templates) falls back silently to client-side rendering. Guarded routes are never rendered on the server — a guard is an authorization point and runs client-side, so the outlet ships empty. <wcs-link> anchors arrive server-rendered with active / aria-current already set.

ルーターにenable-ssrを置き、@wcstack/serverrenderToString(html, { url })にリクエストURLを渡す。初期ルートはサーバー側で描画され — 型付きパラメータも、ネストしたルートも、ルート内容のfor: / if:テンプレートも含めて — クライアントのルーターは起動時にそのDOMを再描画せず引き継ぐ。stateバインディングは同じノードの上でライブにハイドレートされる。検証に失敗したもの(URL不一致、編集されたテンプレート)は黙ってクライアント描画にフォールバックする。ガード付きルートはサーバーでは決して描画されない — ガードは認可点でありクライアントで走るから、outletは空で届く。<wcs-link>のアンカーはactive / aria-currentつきでサーバーから届く。

  • Adopt, Don’t Re-render
  • Route Content Hydrates Live
  • Guarded Routes Ship Empty
  • Silent CSR Fallback
v1.32.0 binder protocol

Markup binds whenever it arrives

What if a binding worked no matter when its DOM showed up?

A data-wcs binding used to exist only for nodes state walked at startup. Route content inactive at that moment, and every <wcs-head> child (reflected into <head> as a clone), silently did nothing — forever. The binder protocol is the fix, and it is one function: whoever inserts DOM hands the subtree to whoever owns bindings. State installs a binder on a global symbol, the router looks it up, neither imports the other — the dual of the transition-runner. A per-route translated <title data-wcs> now just works. The two rejected alternatives are on record: state scanning every insertion would bill the many insertions carrying no bindings, and would let external innerHTML activate data-wcs — against the CSP direction.

data-wcsバインディングは、stateが起動時に走査したノードにしか存在しなかった。その瞬間に非活性だったルート内容も、クローンとして<head>へ反映される<wcs-head>の子も、黙って何もしなかった — 永遠に。binderプロトコルがその修正で、中身は関数1つ。DOMを差し込んだ側が、バインドを所有する側へサブツリーを手渡す。stateがグローバルsymbolにbinderを置き、routerがそれを引く。互いにimportしない — transition-runnerの双対。ルートごとに翻訳された<title data-wcs>が、ただ動くようになった。棄却した2案も記録にある。stateが全挿入を走査する案はバインディングを持たない大多数の挿入に課金し、外部由来のinnerHTMLdata-wcsを発火させてしまう — CSPの方向性に反する。

  • One Function, One Symbol
  • Route Content Binds
  • <title data-wcs> Works
  • No Import Either Way
v1.32.0 @wcstack/view-transition

Animation as policy, not markup

What if leave and move were the only animations you had to ask for?

Package 45. Enter animations never needed JS — @starting-style covers a new row or a mounting branch. What CSS cannot reach is leaving (nodes detach synchronously) and moving (a reorder has no intermediate state). <wcs-view-transition> is a policy node: it renders nothing and describes no animation — it decides whether a DOM change animates and what a collision does (mode="latest / queue / exhaust"), while the animation stays in CSS against ::view-transition-*. The rule everything is subordinate to: a DOM change is applied exactly once, whatever happens to its animation — unsupported browser, hidden tab, reduced motion, collision, all still end with the change applied. naming="auto" names every list row so ::view-transition-group(*.wcs-row) addresses them all.

45番目のパッケージ。入場アニメーションにJSは要らなかった — 新しい行やマウントされる分岐は@starting-styleで足りる。CSSが届かないのは退場(ノードは同期的に外れる)と移動(並べ替えに中間状態はない)。<wcs-view-transition>ポリシーノードだ。何も描画せず、アニメーションも記述しない — DOM変更をアニメーションさせるか、衝突時にどうするか(mode="latest / queue / exhaust")だけを裁定し、アニメーション自体は::view-transition-*のCSSに残る。すべてが従う規則: DOM変更はアニメーションがどうなろうと正確に1回適用される — 非対応ブラウザでも、隠れたタブでも、reduced motionでも、衝突しても、変更は必ず適用されて終わる。naming="auto"は全リスト行に名前を振り、::view-transition-group(*.wcs-row)で一括指定できる。

  • Enter Was Always Free
  • Applied Exactly Once
  • CSS Owns the Animation
  • naming="auto" for Rows
v1.32.0 typedParams · searchParams · replaceUrl

The router speaks its whole state

What if no app ever parsed a path string again?

<wcs-router> now exposes the entire navigation state over wc-bindable: typedParams (a :id(int) arrives as a number), searchParams, routeName, plus replaceUrl beside navigateUrl — so a filter writes replaceUrl = "?category=audio" and never grows the history stack. Events fire in a documented order with path last as the “navigation finished” signal; the exposed objects are frozen snapshots. Query-only navigations are same-match: no guard re-runs, no restamp, no scroll. The route elements themselves became detached controllers — the router element is the one observation surface, and the router-spa demo dropped its last path regex for it. State never interprets a path string again.

<wcs-router>がナビゲーション状態の全体をwc-bindableで公開するようになった。typedParams(:id(int)は数値で届く)、searchParamsrouteName、そしてnavigateUrlの隣にreplaceUrl — フィルタはreplaceUrl = "?category=audio"と書けば履歴を積み上げない。イベントは文書化された順で発火し、最後のpathが「ナビゲーション完了」の合図。公開オブジェクトは凍結されたスナップショット。クエリのみのナビゲーションはsame-match: ガードは再実行されず、再スタンプも、スクロールもない。ルート要素自体はdetachedなコントローラになり、ルーター要素が唯一の観測面 — router-spaデモは最後のパス正規表現をこれで捨てた。stateが二度とパス文字列を解釈することはない。

  • Params Arrive Typed
  • replaceUrl for Filters
  • Query-only = Same-match
  • No Regex Left in the Demo
v1.32.0 <html lang> · basename

The page’s language is the locale

What if lang="ja" was all the i18n config there was?

Breaking: the four locale-dependent filters (locale / date / time / datetime) now default to <html lang> instead of 'en' — the standard place a page records its language becomes the single source of truth, and the CDN one-liner gains a way to set the locale at all. Changing it later re-renders nothing: set the language before the page renders. For multilingual sites, the locale goes in the basename, never in a /:lang route parameter — the router intercepts every same-origin navigation under its basename, so a parameterized language link would switch nothing, silently; with <base href="/ja/"> a link to /en/… falls outside the router and the browser performs the real navigation a language switch needs.

破壊的変更: ロケール依存の4フィルタ(locale / date / time / datetime)の既定が'en'から<html lang>になった — ページが言語を記録する標準の場所が唯一の情報源になり、CDNワンライナーにロケールを設定する手段が初めてできた。後から変えても何も再描画されない。言語はページが描画される前に決める。多言語サイトではロケールはbasenameに置く。/:langルートパラメータには決して置かない — ルーターはbasename配下の同一オリジンナビゲーションをすべて横取りするので、パラメータ化された言語リンクは黙って何も切り替えない。<base href="/ja/">なら/en/…へのリンクはルーターの外に落ち、言語切替に必要な本物のナビゲーションをブラウザが行う。

  • Breaking: 'en' → lang
  • One Source of Truth
  • Locale in the Basename
  • Set It Before Render
v1.32.0 a11y phases 0–7

Accessible by delegation, opt-in by policy

What if the framework’s a11y answer was mostly “let the browser”?

Eight phases, one principle: delegate to the platform, and make anything opinionated opt-in. The router writes out the Navigation API defaults as intent (scroll and focus reset after-transition), repairs scroll in fallback browsers, and offers focus="heading" / announce="title" policies. <wcs-link> pairs its active class with aria-current="page" and forwards aria-* to its anchor. State preserves focus across keyed reorders via moveBefore() — the row you are typing in keeps its cursor through a re-sort. <wcs-raf> gains an opt-in reduced-motion="pause" gate (opt-in because a tick is functional output, not decoration). And a new diagnostic, wcs/aria-attr-unknown, catches the attr.aria-labels typo that setAttribute writes happily and assistive technology ignores.

8フェーズ、原則は1つ。プラットフォームに委譲し、意見のあるものはopt-inにする。ルーターはNavigation APIの既定値を意図として明記し(スクロールとフォーカスリセットはafter-transition)、フォールバックブラウザのスクロールを修繕し、focus="heading" / announce="title"ポリシーを提供する。<wcs-link>activeクラスにaria-current="page"を並走させ、aria-*をアンカーへ転送する。stateはmoveBefore()でキー付き並べ替えを越えてフォーカスを保持 — 入力中の行は再ソートしてもカーソルを失わない。<wcs-raf>にはopt-inのreduced-motion="pause"ゲート(tickは装飾ではなく機能出力だからopt-in)。そして新診断wcs/aria-attr-unknownが、setAttributeは喜んで書き支援技術は無視するattr.aria-labelsのタイプミスを捕まえる。

  • Delegate First
  • focus= / announce= Opt-in
  • moveBefore Keeps Focus
  • aria Typos Caught

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 stringuser.name, cart.items.*.subtotal, @shared. 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   "@app"        Comp B      Named 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-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 semanticsstate, 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.
      $streams: {
        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.

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

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>インラインスクリプトに補完・診断・ホバーを提供。