WcStack IntelliSenseA VS Code extension for @wcstack/state 4.0. Provides TypeScript language features for
The same validator core is shipped headlessly as the FeaturesInline Script Type SupportTypeScript completions work inside
The wrapper's preamble knows the runtime API surface: Attribute Binding CompletionsCompletions for property names, state paths, modifiers, and filter names in
Path candidates are derived from the The row shape of a list that starts as for-context CompletionsInside
Pattern paths ( Template Syntax SupportCompletions and diagnostics also work in Mustache @wcstack/state 4.0 still binds comment bindings — even with
wcs-* Tag Completions (HTML Custom Data)The extension ships
Other editors that use the standard HTML language service (and VS Code setups
without this extension) get the same completions by copying the file into the
project and referencing it from the
Hover, Go to Definition, Find References, Inlay HintsBinding paths are runtime identifiers written directly in HTML, so navigation works without source maps. All four features are queries over the same positional reference index that powers diagnostics.
Hover text follows DiagnosticsReal-time validation of Severity policy: error = the runtime raises or the binding can never work; warning = it runs but silently does the wrong thing (the class of bug these checks exist for); info = advisory. Nothing inside an element marked Deliberate exception — the 4.0 runtime throws at init, but the finding is a warning: The Binding expressions (
|
| Code | Detects | Severity |
|---|---|---|
wcs/binding-path-missing |
Path not found in the state (textContent: typo). Skipped when no path candidates can be derived, and for a path in a state the validator cannot read: under the mount path of a volume whose source cannot be read (<wcs-state mount="cart" src="./cart.js"> when cart.js cannot be read — no file reader, a URL, a leading-/ path, a missing file; a script that is not a plain export default { … } literal, or has a top-level spread; or a <wcs-state> with no source, which waits for setInitialState()), on the ancestors of that mount path (shop of shop.cart), and outside every volume when the page's root state cannot be read (a <wcs-state> inside a <template> or with bind-component is another tree and does not count). An empty state that was read (json='{}', export default {}) is read: its paths are reported. A volume's src= that can be read (.js tries the same-named .ts first) adds its keys and its methods under the mount path (4.0 grafts methods: onclick: cart.add). With a stateSchema the schema decides instead, except for an event handler in a state that cannot be read (the schema carries no methods). A numeric index path (items.0.name, groups.0.items.1.v, groups.*.sel.0.id) is looked up with its indexes read as * — 4.0 reads any number of indexes as the row at that position and follows writes, so the path itself is not a finding (warning). $0, $01 or $1000 — not a loop index (no $1…$128 form), and the $ namespace holds no state path: the runtime fails the binding with this code, inside a for or not (error, reported even without path candidates) |
⚠ / ❌ |
wcs/index-param-range |
$129 (or up to $999) inside a for — a loop index past the limit; the runtime fails the binding with this code. The limit, $128, comes from the manifest (syntax.indexParam.maxDepth). Outside a for it is the loop-index-outside-for warning (wcs/wildcard-rank), since the runtime throws #1401 first. Also this.$0 / this.$129 / this.$1000 in the <wcs-state> script (see below) |
❌ error |
wcs/path-nonexistent |
Same, but the state is declared in a sidecar stateSchema, so absence is definite. A numeric segment is read as a row (*) only where the schema has an array; under an object it is a plain key, as at runtime (sales.2024.totl and an undeclared sales.2025 are reported) |
❌ error |
wcs/path-type-mismatch |
for: on a path the stateSchema proves is not an array |
❌ error |
wcs/binding-type-expectation |
Non-array for: (error); non-boolean class., non-string attr. / style. (warning). if: / elseif: take any value: 4.0 picks the first branch whose condition is truthy |
❌ / ⚠ |
wcs/filter-unknown |
Unknown filter name (count\|fake) — as the 4.0 runtime reports it. This includes the filters 4.0 removed: the 3.2 old names (uc → upper, fix → toFixed, …; the message names the replacement) and substr, folded into slice — the message says to write slice(start, start + length) (with the literal result, substr(2, 3) → slice(2, 5), when the arguments are non-negative literals) |
❌ error |
wcs/filter-arity |
Too few / too many filter arguments (count\|mul) |
❌ error |
wcs/filter-arg-type |
A numeric argument that does not read as a number (count\|gt(abc)) — the runtime throws #27 when the bindings are planned (error; a quoted gt('5') reads as 5, as at runtime). Other mismatches, such as a number for a string argument (join(1)), which the runtime passes as a string (warning) |
❌ / ⚠ |
wcs/filter-input-type |
Filter chain input type mismatch (count\|upper) |
⚠ warning |
wcs/token-undeclared |
command.<method>: $command.<name> with a name not in $commandTokens — the runtime throws #1302 when it attaches the binding (error). $command.<name> in an event binding or eventToken.<prop>: <name> not declared — logged as #1301 each time the event fires (warning). In a state's script, this.$command.<name> (or a $on handler's state.$command.<name>) with a name not in $commandTokens — $command holds the declared tokens only, so .emit() throws a TypeError that names nothing (error, with a did-you-mean) |
❌ / ⚠ |
wcs/token-misconfigured |
command.<method>: right-hand side that is not a $command.<name> — the runtime throws #1201 when the bindings are planned (error). A native element's command.<method>: whose method is not in the manifest's nativeCommands (the native-commands feature's table: showModal on <dialog>, focus on any element, …; with a "did you mean") — the runtime throws #1205 at initialization (error) |
❌ error |
wcs/template-syntax |
Structural directive (for / if / elseif / else) combined with other bindings; spread with a filter or without a target; outerHTML: / outerText: inside a for / if template — a row or branch keeps its nodes by position, and 4.0 throws at init (#203; in a route or layout template too, not in a top-level <template> that only app code clones) (error). A filter on an event handler (onclick: fn\|gt(10)); #direct on a binding that is not an on*: event binding (ignored) (warning). {{ }} outside a <template> (FOUC) and <!--@@:--> visualization (info) |
❌ / ⚠ / ℹ |
wcs/delegated-current-target |
An on*: binding of an event 4.0 delegates to the root (click, dblclick, input, change, submit, keydown, keyup, mousedown, mouseup, pointerdown, pointerup) whose handler — a method of the root state (pick) or of a volume (cart.pick: 4.0 grafts a volume's methods and invokes them the same way) — reads currentTarget from its event parameter before its first await resumes (acorn): there it is the root, not the element. Write on*#direct: (keeping the other modifiers) or use event.target.closest(…). Not reported for #direct, for input / change / submit on a custom element (it may dispatch them without bubbling, heard on the element), inside a <template> with its own <wcs-state> (declarative shadow root, DCC), or inside a top-level <template> the state does not bind (one that only app code clones); router route templates and <wcs-layout layout> templates are checked — the same scope as #203 |
⚠ warning |
wcs/submit-without-prevent |
onsubmit: on a <form> without #prevent, whose handler — a method the page declares — does not call preventDefault: the browser submits the form too, so the page reloads, or with the router a GET puts the fields (a password too) in the URL and the history. Silent on a form with an action, and when the handler cannot be read |
⚠ warning |
wcs/binding-syntax |
What the runtime parser rejects since @wcstack/state 3.0 (error): an unterminated quote in filter arguments, a second # (value#ro#wo — write value#ro,wo), a value after else:, modifiers or left-side filters on for / if / elseif / else / ..., an empty filter (x|, x||y), and (4.0, #120) a state path through __proto__ / prototype (a.__proto__.x — not $command.<name>, an event token, or a method named alone, as at runtime). Judged by the canonical parser (@wcstack/state/parser), so lint and the runtime agree; quoted ; / | in filter arguments are not separators |
|
wcs/wildcard-rank |
A pattern path items.*.name, a shorthand .name or a loop index $1 outside <template for> (4.0 throws #1401 / #1402 at init — the code is the runtime's); more * ranks (or a higher $N) than the enclosing for nesting provides; or a * that ranges over a list other than the one the enclosing for renders at that level (b.*.y inside for: a — 4.0 throws, #1403; read another list's row in a getter with $resolve(path, indexes)) |
⚠ warning |
wcs/index-arity |
$getAll / $setAll / $resolve index count does not match the * count of the path — the runtime throws #901 / #902 where it is called |
❌ error |
wcs/path-readonly |
A binding that writes back — value: on an <input> / <select> / <textarea>, checked: on a checkbox or radio, radio: / checkbox:; not with #ro — on a getter without a setter: every edit throws "<path>" is a getter without a setter, and the state keeps its value. Add a setter, or write value#ro: |
❌ error |
wcs/aria-attr-unknown |
attr.aria-* name that does not exist in WAI-ARIA (with a "did you mean" suggestion) |
⚠ warning |
Built-in wcs-* tag contracts
| Code | Detects | Severity |
|---|---|---|
wcs/tag-member-unknown |
Binding to a property / command. / eventToken. key the tag does not declare in wcBindable — the runtime refuses it with #38 / #1203 / #1204 (at the root the state fails to initialize, in a row the binding fails). That includes DOM properties such as hidden, title, id and textContent; only the HTML sinks (html:, innerHTML:, outerHTML:, srcdoc:) are written without a declaration. A custom element the page defines itself is checked the same way: customElements.define("tag", Class) in an inline script, an src= module or a module it imports by a relative path, whose class (or a superclass the page defines) declares static wcBindable as a literal — properties / inputs / commands lists of { name: "…" }. An element whose declaration is computed, spread or inherited from elsewhere is not checked, nor a dotted property on it (a component's state.x: wiring) |
❌ error |
wcs/handle-bound-as-value |
An output its tag declares semantics: "handle" — <wcs-camera>'s streamReady, the live MediaStream — bound to a state path (streamReady: cam.stream). It works, but puts an object no snapshot, DevTools view or SSR can carry into the state; receive it with eventToken.streamReady: and hand it to the next element as a command argument |
⚠ warning |
wcs/on-prefixed-member |
A tag member whose name starts with on (once on <wcs-timer>), bound without the leading dot: the runtime makes it an event binding and the value never arrives. Write .once: (@wcstack/state 3.1) |
⚠ warning |
wcs/spread-no-bindable |
...: spread onto a helper tag without wcBindable (wcs-fetch-header, wcs-fetch-body, wcs-infinite-scroll, wcs-voice) — the runtime raises |
❌ error |
wcs/trigger-seeded-truthy |
A trigger slot seeded with true (fires immediately, no edge) |
⚠ warning |
wcs/storage-seed-clobber |
Non-manual <wcs-storage> value bound without #init=element to a slot seeded with a literal (null, '', [], or an object with content alike), or to an accessor pair (a getter with a setter — the restore form) — the initial write-back overwrites the stored value on every load, so it is data loss. The fix is value#init=element: (with a null seed for a data slot). A getter without a setter is not reported (it may be save-only), nor is an undefined seed or an expression |
❌ error |
wcs/storage-init-seed |
<wcs-storage> value#init=element: bound to a slot seeded with a literal other than null (cart: [], theme: "light") — #init=element pulls the element's value in, and an empty key loads as null, so the seed is gone on a first visit (this.cart.findIndex(…) throws). Seed null and supply the default where you read it, or bind an accessor pair whose setter ignores null |
⚠ warning |
wcs/input-nested-write |
Non-manual <wcs-storage> value bound to a path the page writes below (value: cart with this[`cart.${i}.quantity`] += 1, this["cart.0.quantity"] = 2, or value: .quantity in a for: cart row): a binding is re-applied only when its path itself is assigned, so those writes are never saved — no error, and a reload brings back the old value. Bind a getter that reads the fields by path (a row getter and $getAll) and returns a new value, or replace the array instead of writing below it. manual is exempt: its trigger save reads the current object. Also <wcs-fetch>'s body bound to such a path (body: newUser with value: newUser.name), manual or not: body resets to null after every request, so the second one goes without it — build a new object in the handler that starts the request. An error: the data is lost and the runtime says nothing (a page that also replaces the whole path afterwards does not need the write below it) |
❌ error |
<wcs-state> script
| Code | Detects | Severity |
|---|---|---|
wcs/nested-assign |
this.user.name = x, +=, ++, expression-index chains — not reactive; use this["user.name"] = x. Also an in-place change the patterns above do not spell with this.: Object.assign(this.form, v), a change through an alias of a state value (const cards = this.cards; cards[0].title = x, const { form } = this), and a write two segments deep through a $on handler's state argument (state.board.cards = x). The message names the path to assign a new value to |
❌ error |
wcs/array-mutation |
push / splice / sort … (9 destructive methods) — not reactive; the message names the non-destructive alternative (concat, toSpliced, toSorted …). Also through an alias (const cards = this.cards; cards.push(x) — assigning the same array back adds no row) and through a $on handler's state argument (state.cards.push(x)) |
❌ error |
wcs/array-index-assign |
this.items[0] = x (bracket-only chain, all compound forms) — use this["items.0"] = x or with(). Also through an alias (const cards = this.cards; cards[i] = card; this.cards = cards — the same array back does not replace the row) and through a $on handler's state argument |
❌ error |
wcs/getter-cycle |
Path getters that reference each other in a cycle — the runtime throws #701 when they are evaluated, and every binding that reads them fails. The check does not follow conditions, so a cycle behind mutually exclusive branches is reported too | ❌ error |
wcs/getter-untracked-read |
this.form.name inside a getter — only form is tracked, so the getter never re-runs when form.name changes; reported only when the document writes form.name somewhere (value: / checked: / spread / an I/O node output / this["form.name"] = …, or state["form.name"] = … in a $on handler — where SSE and WebSocket data land), so a root that is only ever replaced wholesale (router params, $stream folds) stays quiet. Read this["form.name"] instead. Also a getter that reads the URL from location (location.pathname, window.location.search, new URL(location)): it is not state, so the getter keeps the URL it first read — bind <wcs-router>'s path / searchParams. location.protocol / host / origin do not change within a page and are not reported. Also a getter that reads the clock — Date.now(), a new Date() of no argument, performance.now(): the time is not state, so the cached getter keeps its first value (an event token firing every frame does not re-run it). Keep the time in state (the value <wcs-raf> or <wcs-timer> writes) |
⚠ warning |
wcs/on-handler-this |
this.… in a $on handler. The runtime calls it as handler(state, event, …), so this is undefined and the handler throws when called — unlike $watch handlers and methods. Write name(state, event) { state.x = … } |
❌ error |
wcs/updated-callback-unbound |
$renderedCallback tests a path that no binding reads — the branch never runs (the callback is binding-driven). Also a path the state does not have, beside a near one it has (paths.includes("labell") and label, items.*.onn and items.*.on): it is never in paths either |
⚠ warning |
wcs/command-token-unbound |
A token $commandTokens declares, emitted — .emit() in the script, or on*: $command.<name> in the markup — that no command.<method>: $command.<name> binding in the document receives: the emit reaches nothing, without a word. Only the page's root states (a volume's $commandTokens are ignored) |
⚠ warning |
wcs/write-path-missing |
this.<key> = … (or +=, ++, …) of a key the state does not declare, beside a near one it does (this.selectedID and selectedId): the write creates a new key, and nothing bound to the declared one changes. A new key with no near name is not reported — a state may add keys on purpose |
⚠ warning |
wcs/read-path-missing |
A path a getter, setter or method reads through this — this["items.*.amout"], this.totl, this.$getAll("items.*.amout", []), $resolve / $dependOn with a literal path — that the state does not have, beside a near one it has: a nested read gives undefined and a wrong value without a word (the runtime's diagnostics add-on warns once with the same code, once a getter has read it); a top-level key throws on read (#301). Looked up as a binding's path is, under the mount path for a volume's script. Only with a near name — this["opts.flag"] ?? false reads an optional key on purpose — and never a path built at run time (this[key], a template with ${…}). Warning; error where a stateSchema says the path is not there |
⚠ / ❌ |
wcs/path-dynamic-unknown |
A top-level spread in export default { … } (...defaults()) with no stateSchema: the keys it brings in cannot be read, so no path of that state is checked — said once, at the spread, with what turns the checks on (a stateSchema, which wcs-schema derives from JSDoc or TypeScript types). A limit of the lint, not a mistake |
ℹ info |
wcs/name-alias |
A dependency API name that 3.2 renamed and 4.0 removed ($trackDependency → $dependOn, $untrackDependency → $untracked) — the runtime throws when it is read (its diagnostics feature, which @wcstack/state and /auto include, detects it); the message suggests the canonical name |
❌ error |
wcs/declaration-alias |
A declaration key that 3.2 renamed and 4.0 removed ($streams → $stream, $updatedCallback → $renderedCallback) — the runtime throws at load time with its diagnostics feature (a volume, mount=, is refused at graft instead and reported with console.error). Warning when the script cannot be parsed statically (a class state, found by a pattern) |
❌ / ⚠ |
wcs/declaration-alias-read |
Reading a removed declaration key (this.$streams) — 4.0's state has no such key, so the read is silently undefined. Warning when the AST proves the read; info when the script cannot be parsed statically |
⚠ / ℹ |
wcs/behavior-invalid |
$behavior (4.0) that the runtime rejects at load time: not an object, a key the manifest's behaviorOptions does not list (now enableMustache / sameValueGuard / enableDirectionalInitialSync; with a "did you mean"), a value of another type than the option's (all boolean now), or $behavior declared in a volume (mount=) |
❌ error |
wcs/feature-unknown |
A name in $features or in the root <wcs-state features="…"> that is not an add-on (the manifest's features: formats, diagnostics, temporal, list-keys, scopes, recursion, ssr, devtools, native-commands; with a "did you mean") — the runtime throws. A $features name fails as wcs/feature-unknown on a split auto page and as wcs/feature-not-installed elsewhere; the features= attribute is read only by the split auto (dist/split/auto.js), which fails with wcs/feature-unknown (other entries ignore the attribute) |
❌ error |
wcs/features-invalid |
$features that is not an array, or declared in a volume (error); features= on a <wcs-state> that is not the document's root — the runtime reads it only on the first <wcs-state> without mount and bind-component (warning) |
❌ / ⚠ |
wcs/index-param-range |
this.$0 / this.$129 / this["$1000"] in a getter, a method or a $watch handler — 4.0 throws when a $ followed by a digit is read and is not $1…$128. Warning when the script cannot be parsed statically (a class state, found by a pattern) |
❌ / ⚠ |
wcs/watch-declaration-invalid |
$watch key the runtime rejects: $-prefixed, empty segment, non-function handler literal |
❌ error |
wcs/watch-path-missing |
$watch key that does not exist in the state — the handler silently never fires. A key of the page's root may name a path under a volume ("cart.total" — it fires on writes to the grafted state): it is checked against the volume's keys, and not reported when the volume's source cannot be read (as for bindings). A volume belongs to the innermost enclosing <template> that holds a root <wcs-state> (a declarative shadow root, a DCC) and is checked by that root's $watch; any other volume — in the document, or in a route or layout template — belongs to the page's root. (A volume's own $watch is not checked: the volume is refused as a whole — wcs/volume-declaration.) |
⚠ warning |
wcs/scan-declaration-invalid |
$scan — removed in 4.0: the runtime throws at load time with its diagnostics feature (in a mounted component too; a volume is refused at graft and reported with console.error). Fold a path's changes in a $watch handler, or events in an $on handler. One finding on the key; warning when the script cannot be parsed statically (a class state). (The 3.x checks of the entries, wcs/scan-source-computed and wcs/scan-path-missing, are gone with it.) |
❌ / ⚠ |
wcs/type-annotation |
JSDoc @type incompatible with the initial value (see below) |
⚠ warning |
$listKeys declarations are consumed rather than diagnosed: malformed keys (empty path, trailing *, non-flat field) simply produce no path candidates, so a broken declaration is never confirmed by the static side.
Page setup
| Code | Detects | Severity |
|---|---|---|
wcs/script-order |
@wcstack/devtools/auto loaded after @wcstack/state/auto: the DevTools timeline starts when it attaches (4.0 sends it the wiring that is there already) |
ℹ info |
wcs/base-href-missing |
router/auto present without <base href> (basename would be mis-derived). Pinned URLs count (@wcstack/router@4/auto, jsDelivr's @wcstack/router@4.0.2/dist/auto.min.js), and so does the wcstack/auto bundle on a page that has a <wcs-router> |
⚠ warning |
wcs/router-outlet-misplaced |
A <wcs-outlet> that is not right after a <wcs-router>, on a page where some router has no outlet right after it. A router uses only the element right after it and otherwise creates its own outlet there, so the routes render there and this outlet stays empty (docs/router-outlet-placement.md) |
⚠ warning |
wcs/route-bare-mustache |
{{ }} in text directly under <wcs-route> (bound only on the landing route; a route entered by navigation shows it as written), or directly under a layout's <template> (not bound on a navigation). Wrap it in an element |
⚠ warning |
wcs/layout-slot-missing |
A <wcs-layout layout="X"> that holds content outside its named slots — the nested routes — while <template id="X"> has no default <slot>: the content is not shown. The outlet warns with the same code at runtime |
⚠ warning |
wcs/signals-dual-entry |
@wcstack/signals and @wcstack/signals/dom on the same page (duplicated reactive core) |
❌ error |
wcs/signals-dual-entry |
An import map that maps both @wcstack/signals and @wcstack/signals/dom to CDN bundles, though the page imports one: a module it loads (a sidecar .js) that imports @wcstack/signals gets a second core. Map only /dom (it exports the whole core) |
⚠ warning |
v2 migration
| Code | Detects | Severity |
|---|---|---|
wcs/named-state-deprecated |
Named states — <wcs-state name="x">, path@x, {{ path@x }} — removed in v2; the message points to <wcs-state mount="x"> and the prefixed path x.path |
❌ error |
wcs/mount-path-invalid |
mount= value the runtime rejects: empty, an empty segment, * anywhere in a segment, whitespace, or the reserved characters $ # @ |
❌ error |
Volumes, component mounts and roots (4.0)
| Code | Detects | Severity |
|---|---|---|
wcs/volume-declaration |
A declaration a volume (<wcs-state mount="…">) does not take — the lists are 4.0's scopes/volume.ts. $stream, $watch, $listKeys, $renderedCallback: the runtime refuses to graft the volume and reports it with console.error — the state is not on the tree (error). $commandTokens, $eventTokens, $on, $errorCallback: the runtime says so with console.warn and ignores the declaration — they belong to the root (warning). One level lower when the script cannot be parsed statically (a class state). The other keys the runtime refuses in a volume keep their own codes ($scan, $recursion, $behavior, $features, the old names $streams / $updatedCallback). A value of literal undefined is no declaration, as at runtime |
❌ / ⚠ |
wcs/bind-component-source |
A <wcs-state bind-component> with a state / src / json attribute or an inline <script type="module">: a mounted component's state is the host element's property only, and the runtime refuses to load it (console.error; the component does not mount). Reported on the start tag; the script inside is not checked further, since the runtime never reads it |
❌ error |
wcs/second-root |
A second <wcs-state> without mount and bind-component in the document (outside <template>): there is one state tree per root, and the runtime refuses the one that loads second (console.error). Graft a subtree with <wcs-state mount="path">. Reported on each further start tag |
❌ error |
Sidecar manifest (wcstack.manifest.json)
All wcs/manifest-* and wcs/drift-* findings are errors, except wcs/manifest-namespace-version (warning) and wcs/manifest-override (info — an intentional, declared shadow). Codes: manifest-broken, manifest-schema-version, manifest-kind-invalid, manifest-unknown-keyword, manifest-external-ref, manifest-ref-cycle, manifest-ref-unresolved, manifest-namespace-version, manifest-tag-collision, manifest-filter-collision, manifest-state-collision, manifest-override, drift-missing-member, drift-event-mismatch.
One further code (wcs/path-reserved-name) is reserved in the code table but not emitted yet. The single source of truth for codes is src/core/diagnostics.ts.
The codes are the runtime's: a message the 4.0 runtime prints in the console carries the same [wcs/…] code. Without the diagnostics add-on (@wcstack/state/core alone) the runtime prints only a message number and the values — [@wcstack/state] [#203](https://github.com/wcstack/wcstack/issues/203) "outerHTML" (the hundreds of the number name the code: 2xx is wcs/template-syntax) — and the full auto bundle (which installs the add-on) prints the code and the sentence. Some runtime codes depend on what only the running page knows and have no static counterpart: wcs/feature-not-installed (which entry and installFeatures the page uses), wcs/recursion-context, and the wcs/template-syntax #204 case (a structural template at the top of content inserted later). The wcs/mount-dollar-declaration warning ($watch / $stream / $renderedCallback on a mounted component) has none either: that state is the component's JavaScript property, which the validator does not read — a script inside <wcs-state bind-component> is never loaded (wcs/bind-component-source).
JSDoc Type Validation
Validates consistency between @type annotations and initial values (wcs/type-annotation):
/** @type {string} */
label: null, // ⚠ Type "null" is not compatible with @type {string}
/** @type {string|null} */
label: null, // ✅ OK
The same @type also declares a shape. A list that starts as null (or []) and is filled by <wcs-fetch> or <wcs-storage> has no row to read its fields from, so its rows would be reported as wcs/binding-path-missing; with an object type for the rows, catalog.*.id and catalog.*.name exist (with their types), and a typo under them is still reported. Name the row type with a @typedef in the same script if you like:
/** @typedef {{ id: number, name: string, price: number }} Product */
export default {
/** @type {Product[] | null} */
catalog: null, // for: catalog → .id / .name / .price
get products() { return this.catalog ?? []; }, // a getter's result keeps the shape
};
The types understood are a subset of TypeScript's JSDoc: names, object types ({ a: T; b?: U }), T[] / Array<T>, unions, ?T, and @typedef names (a self-referencing typedef stops at the nesting limit). A type the validator cannot read (a function type, Record<…>) declares no shape; any / unknown / * are not checked against the initial value.
Sidecar Manifest, stateSchema, and the CLI
Sidecar manifests. wcstack.manifest.json files are validated against the
supported JSON-Schema subset: envelope / kind checks, cross-file package
resolution, same-name tag/filter collision, forbidden override-after-collision, and
drift against the live static wcBindable surface. The sidecar is tooling-only:
it never overrides the runtime static wcBindable declaration, and a missing or stale
file never changes runtime behavior. The normative schema and resolution rules live in
docs/wcstack-manifest-schema.md.
stateSchema. An application manifest may declare the shape of a state
(wcstack.application.states[<name>].stateSchema). The nearest wcstack.manifest.json
above the HTML file is discovered automatically (one file, no merging). For a declared
state, a missing path is reported as wcs/path-nonexistent (error) instead of
wcs/binding-path-missing (warning), and for: on a schema-confirmed non-array is
wcs/path-type-mismatch. Methods, getters, and $listKeys from the script count as
existing even when absent from the schema; beneath a bare {} the validator stays silent.
Write the schema by hand, or generate it from a TypeScript state file with wcs-schema
from @wcstack/typescript — see
docs/typescript.md. The same package's wcs-tsc type-checks
inline <wcs-state> scripts across a project using this extension's language plugin in
tsc mode.
CLI. A single validateDocument entry point drives both the in-editor diagnostics
and the CLI, so the IDE and CI report identically for the same inputs. Both read the
external state referenced via <wcs-state src="..."> (the root's and the volumes') and
discover the nearest wcstack.manifest.json through the same file reader, relative to
the HTML file. The IDE does so for a document saved on disk (a file: URI); an untitled
or virtual document skips src (its paths are not reported, as for a src that cannot
be read), and an external state file with unsaved edits is read as it is on disk. The script rules (wcs/nested-assign, wcs/array-mutation, wcs/on-handler-this, …) check a .js / .ts external state too: the IDE shows a finding in the file on the page's src value, with the file's line and column in front of the message and a link to the file; the CLI reports it in the file. The bundled wcs-validate CLI runs the same checks headlessly — over
wcstack.manifest.json sidecars (any *.manifest.json argument) and/or HTML
data-wcs bindings — and is distributed on npm as
@wcstack/lint, a zero-dependency
wrapper around the exact same CLI bundle:
npx @wcstack/lint [--attr=data-wcs] [--state-tag=wcs-state] [--lang=ja|en] [--errors-only] [--strict] <file> [<file> ...]
| Option | Effect |
|---|---|
--attr=<name> |
Bind attribute name (default data-wcs) |
--state-tag=<name> |
State element tag name (default wcs-state) |
--lang=ja\|en |
Message language. Default: environment locale (LC_ALL / LC_MESSAGES / LANG, then the OS locale); codes and ranges do not depend on it |
--errors-only (alias --quiet) |
Print only error-severity lines; warning / info counts and the exit code are unchanged |
--strict |
Exit 1 on warnings too. Severities are unchanged — only the exit-code threshold moves. Use it to fail CI on a path typo (wcs/binding-path-missing is a warning) |
Exit code: 0 no error (with --strict: no error or warning) · 1 at least one error (with --strict: error or warning) · 2 usage or file-read failure.
When working on the validator itself, or in this repo's CI (the wcs-validate job runs it exactly this way), build from source and invoke the CLI with node:
# one-time build (from the repo root)
cd packages/vscode-wcs && npm ci && npm run build && cd ../..
node packages/vscode-wcs/dist/cli.cjs [--attr=data-wcs] [--state-tag=wcs-state] [--lang=ja|en] [--errors-only] [--strict] <file> [<file> ...]
Settings
| Setting | Default | Description |
|---|---|---|
wcstack.bindAttributeName |
"data-wcs" |
Bind attribute name |
wcstack.stateTagName |
"wcs-state" |
Custom element tag name for state definition |
wcstack.messageLanguage |
"auto" |
Language of diagnostic and hover messages: auto (VS Code display language; non-Japanese locales get English), ja, or en. Codes and ranges are language-independent |
Requirements
- VS Code 1.110+
- HTML files containing
<wcs-state>elements (tag completions and hover forwcs-*elements work in any HTML file)
Reporting Issues
Use the extension issue form — it asks for the extension and VS Code versions and a minimal <wcs-state> reproduction, and files the report under the @wcstack/vscode-wcs label.
License
MIT