Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,7 @@ MyThing.register('my-thing');

**Async render (`async render()`), bare-await data fetch (#469).** A component may write `async render() { const u = await getUser(this.id); return html\`<h3>${u.name}</h3>\`; }`. Writing `await` makes the function async by JS rule, and every render path awaits a promise-returning `render()` automatically (no flag). This co-locates the fetch in the leaf component (no prop-drilling). The model is decoupled into three separate concerns. (1) **SSR always blocks**, so the resolved DATA is in the first paint with no fallback markup (PE-safe, JS-off reads it). (2) **The client re-fetch default is stale-while-revalidate**: when a prop / dependency change re-runs `async render()`, the current content stays until the new render resolves (no blank, no flash). (3) **`renderFallback()` is the OPTIONAL re-fetch loading UI**, a prop-aware method shown ONLY during a client re-fetch, NEVER on the first paint, and it does NOT trigger SSR streaming. **Errors are isolated per component by default** (no user code): a thrown `await getData()` renders a component-scoped error state while siblings render, and `renderError()` optionally customizes it (dev surfaces the message, prod stays silent). `getData()` is already isomorphic (a `'use server'` action is the real function during SSR and an RPC stub on the client), so the same line works both sides. Use `async render()` for request-time-known SERVER data that should be in the first paint; keep `Task` / signals for genuinely client-only data (a `Task` shows its pending state at SSR, losing first-paint data). A **bare** async-render component (an `async render()` with no other client signal, light DOM) is **elided** like any display-only component (#474): its SSR'd HTML is the complete output, so the framework drops the module AND the redundant on-hydration re-fetch. It SHIPS only when it also carries an independent signal (an `@event`, a non-`state` reactive prop, a signal / reactive import, a lifecycle hook including `renderFallback()`, the dynamic slot READ surface (`slotchange` / `assignedNodes` / `assignedElements` / `assignedSlot`; merely RENDERING a `<slot>` does not ship, since the SSR output carries the placed children), `static shadow = true`, `static interactive = true`, cross-module observation, or a transitively-reachable interactive child). Two carve-outs always ship: `static shadow = true` (Declarative Shadow DOM attaches only during HTML parsing, so a streamed or soft-navigated shadow component needs its module to re-run `attachShadow`) and `static interactive = true` (the explicit author override that forces a ship when the analyser cannot see a component's interactivity statically: an OBSERVER that computes the tag it waits for, a `:defined` rule in an external stylesheet outside the module graph, or a consumer reaching the element through a string selector; a component's OWN registration tag must be a literal per invariant 3, and a computed one is invisible to the scanner, so it gets no verdict and the override cannot rescue it). **For SLOW data where blocking the first byte hurts, wrap the region in `<webjs-suspense .fallback=${html\`…\`}>` to STREAM it** (the fallback flushes on the first byte, the data streams in; multiple boundaries fetch concurrently). This is the only way to show a first-paint fallback, a deliberate choice for slow regions, and it streams progressively on soft navigation too. A throwing component inside a boundary is isolated (renders its error state, siblings stream). **The on-hydration re-fetch is itself eliminated by SSR action seeding (#472):** each `'use server'` action result invoked during a (non-streamed) SSR render is serialized into the page, and the generated RPC stub reads that seed on its first client call, so a shipping async component does NOT re-issue the RPC on hydration (a later refetch / arg-change still goes to the network). Keyed by action-hash + fn + serialized args, consume-once, fail-open (a miss degrades to a normal RPC). A hit returns the value the SSR render that produced THIS page computed for exactly that key, so it cannot disagree with the HTML on screen (a page navigation evicts whatever the outgoing page left unconsumed, in the DOM and in the store, so a departed render's value can never be served); on an HTML-cached page (#241) the seed rides inside the cached bytes and is as fresh as they are. The one shape where a hit can differ from the paint is an action returning a DIFFERENT result for the SAME arguments twice in one render (the seed carries the last, the first component painted the first), which dev warns about once per action function. Captured via a transparent server-side `'use server'` facade (no source transform, no build step; the browser source tab and on-disk files are unchanged), default on, opt out with `"webjs": { "seed": false }` or `WEBJS_SEED=0`. **A miss is otherwise invisible, so dev makes it observable (#1309):** every dev page response carries `X-Webjs-Seed` (`off` / `html-cache` / `collected=<m>, emitted=<n>` / `collected=<m>, emitted=0, streamed`), folded into the access-log line as a `seed` field, and the browser logs ONE warning per page view when a hydration call missed AND the cause is provable (a streamed page, a serializer drop, or seeds present but unmatched), staying silent otherwise, including on a page that emitted no seeds, where a miss is not evidence of a defect because a mutation / `Task` / `connectedCallback` call routes through the same lookup and could never be seeded. `seedStats()` from `@webjsdev/core` exposes the counters. Nothing reaches production: the client's dev gate is a server-stamped `data-webjs-dev` marker on the seed block, never `process.env.NODE_ENV`, which esbuild folds to a constant in the built core bundle. See `references/data-and-actions.md`.

**Light DOM (default) vs Shadow DOM.** Light DOM applies global CSS and Tailwind directly (default; for Tailwind/global CSS + simple composition). Shadow DOM (`static shadow = true`) is for `static styles` scoped CSS and third-party isolation; `<slot>` works in either. **Light-DOM slots ARE the native DOM slot API (#1021, full shadow parity):** `<slot>` works identically in light and shadow DOM, so post-mount native writes are LIVE (`appendChild`, `insertBefore`, `removeChild`, `el.remove()`, `innerHTML`, `el.slot=` flips, `HTMLSlotElement.assign()`) and the reads (`assignedNodes` / `assignedElements` / `{flatten}` / `assignedSlot` / `slotchange`, with native async-coalesced timing) match. Flip `static shadow` and nothing else changes; there is NO WebJs-specific slot API. The one write that does NOT flip is `assign()`: the light-DOM version is an extension (element-bound overlay alongside name matching), while native shadow `assign()` needs `slotAssignment: 'manual'`, which WebJs does not set, so avoid `assign()` in a component meant to flip modes. A FORWARDED slot (a template forwarding `<slot>` into a nested component) projects its content on the client and through hydration (#1023): the renderer stamps each slot with its template owner (`SLOT_OWNER`, carried across SSR as `data-wj-slot-owner`) so it routes to the OUTER host that rendered it, and a layout's `${children}` inside a slotted shell keeps its named slots in sync across a soft-nav swap (#1024, the swap resyncs every own slot of the enclosing host). Four inherent gaps, all a consequence of light DOM having no shadow boundary: structural host reads (`host.children` / the `innerHTML` getter show the rendered template, not the authored children, so read slotted content with `assignedNodes()`), `assignedChild.parentNode` is the `<slot>`, `::slotted()` CSS (style slotted content with normal selectors / Tailwind), and initial-projection lifecycle timing (`firstUpdated` sees the `<slot>` element with EMPTY `assignedNodes()`, the projection lands one microtask later; read assigned content from `slotchange` or after a microtask). Live writes need the component's JS on the page, so a display-only slotted wrapper elides (its writes are inert like anything elided; force a ship with `static interactive = true` for an imperative consumer the analyser cannot see). A light-DOM component authoring custom CSS MUST prefix every class selector with its tag name (invariant 7); prefer Tailwind. **Light-DOM component hosts default to `display: block`**: a custom element is `display: inline` in plain CSS, so the framework marks every LIGHT-DOM host `data-wj-host` and injects one head rule in a low-priority cascade layer, `@layer webjs-host { :where([data-wj-host]) { display: block } }`, so a container component does not collapse; the layer keeps it overridable by any author style INCLUDING Tailwind utilities (`class="flex"`/`grid`/`hidden` win, because their layer is ordered after `webjs-host`), a `[hidden]` carve-out keeps `?hidden` working, and an inline light component opts out with `my-tag { display: inline }`. **Shadow-DOM hosts are NOT marked** (a document rule would override the shadow tree's `:host`), so a shadow component sets its host display via `:host { display: block }` in `static styles` (respected because the host is unmarked; set it for a shadow block container). See the even-grid / no-reflow layout recipes in `references/styling.md`. **Never interpolate into a component's `<style>` or `<script>` body** (`html\`<style>${css}</style>\``): the server emits it but the client drops the raw-text hole, so it paints at SSR then wipes to empty on hydrate. Use `static styles` or Tailwind instead (flagged by `no-interpolation-in-raw-text-element`). A page/layout, which never hydrates, may interpolate a `css` result into `<style>`. Install the `webjs` VSCode extension (`packages/editors/vscode`, VS Marketplace + Open VSX; also covers Cursor / Antigravity / Windsurf) or `webjs.nvim` (`packages/editors/nvim`, via lazy.nvim) for template highlighting + editor intelligence with no Lit plugin, or add the standalone `@webjsdev/intellisense` to `tsconfig.json` `plugins` manually (JetBrains). Full deep-dive in `references/components.md` + `references/muscle-memory-gotchas.md`.
**Light DOM (default) vs Shadow DOM.** Light DOM applies global CSS and Tailwind directly (default; for Tailwind/global CSS + simple composition). Shadow DOM (`static shadow = true`) is for `static styles` scoped CSS and third-party isolation; `<slot>` works in either. **Light-DOM slots ARE the native DOM slot API (#1021, full shadow parity):** `<slot>` works identically in light and shadow DOM, so post-mount native writes are LIVE (`appendChild`, `insertBefore`, `removeChild`, `el.remove()`, `innerHTML`, `el.slot=` flips, `HTMLSlotElement.assign()`) and the reads (`assignedNodes` / `assignedElements` / `{flatten}` / `assignedSlot` / `slotchange`, with native async-coalesced timing) match. Flip `static shadow` and nothing else changes; there is NO WebJs-specific slot API. The one write that does NOT flip is `assign()`: the light-DOM version is an extension (element-bound overlay alongside name matching), while native shadow `assign()` needs `slotAssignment: 'manual'`, which WebJs does not set, so avoid `assign()` in a component meant to flip modes. A FORWARDED slot (a template forwarding `<slot>` into a nested component) projects its content on the client and through hydration (#1023): the renderer stamps each slot with its template owner (`SLOT_OWNER`, carried across SSR as `data-wj-slot-owner`) so it routes to the OUTER host that rendered it, and a layout's `${children}` inside a slotted shell keeps its named slots in sync across a soft-nav swap (#1024, the swap resyncs every own slot of the enclosing host). Four inherent gaps, all a consequence of light DOM having no shadow boundary: structural host reads (`host.children` / the `innerHTML` getter show the rendered template, not the authored children, so read slotted content with `assignedNodes()`), `assignedChild.parentNode` is the `<slot>`, `::slotted()` CSS (style slotted content with normal selectors / Tailwind), and initial-projection lifecycle timing (`firstUpdated` sees the `<slot>` element with EMPTY `assignedNodes()`, the projection lands one microtask later; read assigned content from `slotchange` or after a microtask). Live writes need the component's JS on the page, so a display-only slotted wrapper elides (its writes are inert like anything elided; force a ship with `static interactive = true` for an imperative consumer the analyser cannot see). A light-DOM component authoring custom CSS MUST prefix every class selector with its tag name (invariant 7); prefer Tailwind. **Light-DOM component hosts default to `display: block`**: a custom element is `display: inline` in plain CSS, so the framework marks every LIGHT-DOM host `data-wj-host` and injects one head rule in a low-priority cascade layer, `@layer webjs-host { :where([data-wj-host]) { display: block } }`, so a container component does not collapse; the layer keeps it overridable by any author style INCLUDING Tailwind utilities (`class="flex"`/`grid`/`hidden` win, because their layer is ordered after `webjs-host`), a `[hidden]` carve-out keeps `?hidden` working, and an inline light component opts out with `my-tag { display: inline }`. **Shadow-DOM hosts are NOT marked** (a document rule would override the shadow tree's `:host`), so a shadow component sets its host display via `:host { display: block }` in `static styles` (respected because the host is unmarked; set it for a shadow block container). See the even-grid / no-reflow layout recipes in `references/styling.md`. **Never interpolate into a component's `<style>` or `<script>` body** (`html\`<style>${css}</style>\``): the server emits it but the client drops the raw-text hole, so it paints at SSR then wipes to empty on hydrate. Use `static styles` or Tailwind instead (flagged by `no-interpolation-in-raw-text-element`). A page/layout, which never hydrates, may interpolate a `css` result into `<style>`. Install the `WebJs` VSCode extension (`packages/editors/vscode`, published as `WebJs.WebJs` on the VS Marketplace, and installed in Cursor / Antigravity / Windsurf by downloading its `.vsix` from that listing until an Open VSX listing exists) or `webjs.nvim` (`packages/editors/nvim`, via lazy.nvim) for template highlighting + editor intelligence with no Lit plugin, or add the standalone `@webjsdev/intellisense` to `tsconfig.json` `plugins` manually (JetBrains). Full deep-dive in `references/components.md` + `references/muscle-memory-gotchas.md`.

---

Expand Down
22 changes: 22 additions & 0 deletions changelog/vscode/0.2.6.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
package: "webjs (VS Code extension)"
version: 0.2.6
date: 2026-09-23T11:17:11.543Z
commit_count: 1
npm: false
---
## Changes

- **the extension is now listed as "WebJs", under the `WebJs.WebJs` id**

The Marketplace listing was previously prepared as `webjsdev.webjs` and
briefly published as "WebJs Framework". It is now published as `WebJs.WebJs`
and shows as **WebJs**, which is what the framework is called everywhere
else. Install it with `code --install-extension WebJs.WebJs`, or search
"WebJs" in the Extensions view.

The docs, the README and the publishing guide now point at the live listing;
the old `webjsdev.webjs` links they carried were dead.

Open VSX is unchanged and still has no listing, so Cursor, Antigravity,
Windsurf and VSCodium install the packaged `webjs.vsix` directly for now.
17 changes: 17 additions & 0 deletions changelog/vscode/0.2.7.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
package: "webjs (VS Code extension)"
version: 0.2.7
date: 2026-09-24T05:30:00.000Z
commit_count: 1
npm: false
---
## Changes

- **the Marketplace listing writes the brand as WebJs**

The listing's subtitle and the body of its page both wrote the project
lowercase. Invariant 11 makes `WebJs` a proper noun in prose, including in a
JSON `description` value, and every sibling package already followed it.

Both strings reach users only through a publish, so this ships as its own
version rather than riding along silently.
15 changes: 8 additions & 7 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/editors/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ Framework-wide rules live in the root [`../../AGENTS.md`](../../AGENTS.md).
| Dir | Package | What it is | Ships to |
|---|---|---|---|
| `intellisense/` | `@webjsdev/intellisense` (npm) | The standalone tsserver plugin: in-template completions, diagnostics, go-to-definition, hover. **The SOURCE OF TRUTH for all editor intelligence.** | npm |
| `vscode/` | `webjs` extension | VS Code / Cursor / Windsurf / Antigravity extension. Highlighting (TextMate grammars) + snippets + commands, and it **bundles** intellisense. | VS Marketplace + Open VSX |
| `vscode/` | `WebJs` extension (`WebJs.WebJs`) | VS Code / Cursor / Windsurf / Antigravity extension. Highlighting (TextMate grammars) + snippets + commands, and it **bundles** intellisense. | VS Marketplace (live); Open VSX (not yet) |
| `nvim/` | `webjs.nvim` | Neovim plugin. Treesitter highlighting + `:WebjsCheck` + an LSP helper, and it **vendors** intellisense. | a standalone GitHub repo (`webjsdev/webjs.nvim`) |

Both editor plugins carry their OWN copy of the intellisense plugin (a Neovim
Expand Down
10 changes: 6 additions & 4 deletions packages/editors/vscode/AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
# AGENTS.md for the `webjs` VSCode extension
# AGENTS.md for the `WebJs` VSCode extension

The all-in-one editor extension for webjs, shipping to the **VS
Marketplace** and **Open VSX** (the latter is what Cursor, Antigravity,
Windsurf, and VSCodium pull from). It is phase 1 of the editor-plugin
The all-in-one editor extension for webjs, published as **`WebJs.WebJs`**
on the **VS Marketplace**, and destined for **Open VSX** (what Cursor,
Antigravity, Windsurf, and VSCodium pull from) once it is uploaded there.
`displayName` carries a deliberate trailing U+00A0. See
[`PUBLISHING.md`](./PUBLISHING.md) before touching it. It is phase 1 of the editor-plugin
epic (#381).

Framework-wide rules (workflow, JSDoc-in-`packages/`, no-build, commit
Expand Down
Loading
Loading