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タグに。
What's New
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のページの版ごとの注記にある。
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も閉じる。
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)。
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と同じく要素にリスナーを付ける。
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)。
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周で打ち切られる。
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>が接続したときに配線される。
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はハイドレーションを壊す)。
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で名指しした機能を読み込む。
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にも修正が入る。
The Core Insight
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を読めばすべてが分かる。
The Core
What's missing from the browser — and what we built.
ブラウザに足りないもの — そして我々が作ったもの。
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以上のビルトインフィルタ、双方向バインディング、構造ディレクティブ対応のリアクティブ状態管理。
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管理を備えた宣言的ルーティング。
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に対応。
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ファースト版。
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()を呼ぶだけ。ゼロフリッカーの自動ハイドレーション付き。
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置換モードも。
The I/O Nodes
Thirty-plus declarative wrappers over the Web platform — each one a headless custom element that binds straight into your state.
Webプラットフォームの30以上のAPIを宣言的タグ化。すべてヘッドレスなカスタム要素として、状態に直接バインドされる。
wc-bindableEvery 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からも使える。
See it in action
Because it is HTML.
HTMLのように読める。なぜなら、HTMLだから。
<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>
<!-- 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] -->
<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>
<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>
<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>
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.
<!-- 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>
Constraints
This project follows five strict constraints. They're what make it interesting.
5つの厳格な制約。これが面白さの源泉。
One <script> tag. That's it. No npm, no bundler, no config.
scriptタグ1つ。npm不要、バンドラー不要、設定不要。
Everything is a custom element. If it can't be expressed as <wcs-something>, it doesn't belong here.
すべてはカスタム要素。<wcs-something>で表現できなければ、ここには属さない。
The script just registers custom elements. No initialization code, no bootstrap ritual.
スクリプトはカスタム要素を登録するだけ。初期化コードもブートストラップ儀式も不要。
Expressions live in data-* attributes and text nodes — places HTML already allows extension. The DOM structure and semantics stay intact.
式はdata-*属性とテキストノードに配置 — HTMLが拡張を許可する場所のみ。DOM構造とセマンティクスはそのまま。
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の仕様の中でライフサイクル・順序・通信を解決しなければならない。依存ゼロなら、あらゆるアルゴリズムを自分で書く。そしてそのすべてが「ブラウザ組み込みかもしれない」と感じさせなければならない。
Quick Start
Pick only the scoped packages you need. Element packages ship an /auto entry.
必要なスコープ付きパッケージだけを選ぶ。要素パッケージは/autoエントリを持つ。
<!-- 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>
# 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>インラインスクリプトに補完・診断・ホバーを提供。