feat: install the editor extension from the Marketplace, which never worked - #1487
Conversation
The extension had never been uploaded: the listing it documented, webjsdev.webjs, did not exist. It is now live as WebJs.WebJs and shows as "WebJs" rather than "webjs", matching the framework's own casing, and it carries the new monogram icon. displayName ends in a non-breaking space on purpose. A listing briefly existed under the id webjs-vscode and was unpublished, which retires an id and its display name permanently, so the plain name is refused. The uniqueness check trims ASCII whitespace but not U+00A0. PUBLISHING.md records this so the next person does not "fix" it and break the publish.
Every surface named webjsdev.webjs, which returns 404. The docs site's editor-setup page is the one users follow, so its install link was broken for anyone trying to set up an editor. Open VSX has no listing under either identity and never did, so the surfaces that promised it now say so and tell those editors to install the vsix directly, rather than linking a second dead page.
|
Preview for Next: fix the listed keys in the compose file |
The extension is an npm workspace member, so renaming it and bumping the version leaves package-lock.json stale and every CI job dies at npm ci with "Missing: WebJs@0.2.6 from lock file".
Invariant 11 bans U+2014 repo-wide and the Conventions job enforces it. Introduced by the identity-change docs in this branch.
| <div class="callout"> | ||
| <p><strong>VS Code, Cursor, Windsurf, VSCodium:</strong> install the <strong><code>webjs</code></strong> extension from the <a href="https://marketplace.visualstudio.com/items?itemName=webjsdev.webjs" target="_blank">VS Marketplace</a> or <a href="https://open-vsx.org/extension/webjsdev/webjs" target="_blank">Open VSX</a> (search "webjs"). It bundles the language-service plugin and registers it automatically (no <code>tsconfig.json</code> edit), and adds <code>html</code> / <code>css</code> template highlighting.</p> | ||
| <p><strong>VS Code:</strong> install the <strong><code>WebJs</code></strong> extension from the <a href="https://marketplace.visualstudio.com/items?itemName=WebJs.WebJs" target="_blank">VS Marketplace</a> (search "WebJs"). It bundles the language-service plugin and registers it automatically (no <code>tsconfig.json</code> edit), and adds <code>html</code> / <code>css</code> template highlighting.</p> | ||
| <p><strong>Cursor, Antigravity, Windsurf, VSCodium:</strong> these pull from Open VSX, where the extension is not published yet. Until it is, download <code>webjs.vsix</code> from the <a href="https://github.com/webjsdev/webjs/tree/main/packages/editors/vscode" target="_blank">extension directory</a> and install it with <code>code --install-extension webjs.vsix</code> (substitute your editor's CLI).</p> |
There was a problem hiding this comment.
The vsix this points at is not in the repo. webjs.vsix is gitignored (packages/editors/vscode/.gitignore:2) and no GitHub release carries it as an asset, so a Cursor / Windsurf / VSCodium user who follows this link lands in packages/editors/vscode on GitHub and finds no .vsix to download. With Open VSX unpublished, that leaves those editors with no working install path at all from the docs.
The Marketplace itself serves the package (verified: GET .../publishers/WebJs/vsextensions/WebJs/0.2.6/vspackage returns 200), so the listing's Download Extension link is the instruction that actually works.
| <p><strong>Cursor, Antigravity, Windsurf, VSCodium:</strong> these pull from Open VSX, where the extension is not published yet. Until it is, download <code>webjs.vsix</code> from the <a href="https://github.com/webjsdev/webjs/tree/main/packages/editors/vscode" target="_blank">extension directory</a> and install it with <code>code --install-extension webjs.vsix</code> (substitute your editor's CLI).</p> | |
| <p><strong>Cursor, Antigravity, Windsurf, VSCodium:</strong> these pull from Open VSX, where the extension is not published yet. Until it is, open the <a href="https://marketplace.visualstudio.com/items?itemName=WebJs.WebJs" target="_blank">Marketplace listing</a>, use its <em>Download Extension</em> link to save the <code>.vsix</code>, and install that file with <code>code --install-extension path/to/the.vsix</code> (substitute your editor's CLI).</p> |
| - **VSCode:** search "webjs" in the Extensions view (Marketplace). | ||
| - **Cursor / Antigravity / Windsurf / VSCodium:** search "webjs" (these pull from Open VSX). | ||
| - **VSCode:** search "WebJs" in the Extensions view, or install [`WebJs.WebJs`](https://marketplace.visualstudio.com/items?itemName=WebJs.WebJs) from the Marketplace. | ||
| - **Cursor / Antigravity / Windsurf / VSCodium:** these pull from Open VSX, where the extension is not published yet. Install the packaged `webjs.vsix` directly with `code --install-extension webjs.vsix` (substitute your editor's CLI). |
There was a problem hiding this comment.
"Install the packaged webjs.vsix" never says where to get it, and there is nowhere. The vsix is gitignored and attached to no release, so the only way to obtain one is to clone the monorepo and run npm run package. Since this README is also the Marketplace listing body, the reader most likely to hit this line is the one who cannot use the Marketplace install.
The listing does serve the package for download, so point at that:
| - **Cursor / Antigravity / Windsurf / VSCodium:** these pull from Open VSX, where the extension is not published yet. Install the packaged `webjs.vsix` directly with `code --install-extension webjs.vsix` (substitute your editor's CLI). | |
| - **Cursor / Antigravity / Windsurf / VSCodium:** these pull from Open VSX, where the extension is not published yet. Until it is, use the **Download Extension** link on the [Marketplace listing](https://marketplace.visualstudio.com/items?itemName=WebJs.WebJs) to save the `.vsix`, then install that file with `code --install-extension path/to/the.vsix` (substitute your editor's CLI). |
| **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; 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`. |
There was a problem hiding this comment.
This line now claims coverage the PR itself says does not exist. The old text said "VS Marketplace + Open VSX"; the new one drops Open VSX but keeps "also covers Cursor / Antigravity / Windsurf". Everywhere else in this PR (the README, the docs page, packages/editors/AGENTS.md, packages/editors/vscode/AGENTS.md) says those editors have no listing and must sideload the vsix. AGENTS.md is the always-loaded agent surface, so an agent reading only this will tell a Cursor user to search their Extensions view, which finds nothing.
Suggested wording (the sentence is inside a very long paragraph, so apply by hand):
published as
WebJs.WebJson the VS Marketplace, and installed in Cursor / Antigravity / Windsurf by downloading its.vsixfrom that listing until an Open VSX listing exists
| commit_count: 1 | ||
| npm: false | ||
| --- | ||
| ## Changed |
There was a problem hiding this comment.
Nit, but it is a heading the corpus does not use: the changelog groups are Breaking / Features / Performance / Fixes (changelog/README.md), with Changes as the hand-written variant (5 entries). Changed is a sixth spelling of the same bucket, and test/packaging/changelog-editor-packages.test.mjs:53 already asserts an editor entry carries one of the known headings (pinned to 0.2.0 today, so nothing reds yet).
| ## Changed | |
| ## Changes |
The Cursor / Windsurf / VSCodium instructions told the reader to fetch webjs.vsix from the extension directory on GitHub, but that file is gitignored and no release carries it, so the only registry-less install path the docs offered led nowhere. The Marketplace listing serves the package itself, so point at its Download Extension link instead. Also drops the root AGENTS.md claim that the Marketplace listing covers Cursor / Antigravity / Windsurf, which the rest of this branch contradicts, and spells the changelog heading the way the corpus does.
The listing's subtitle (the manifest description) and the body of its page (the README) both wrote the project lowercase. Invariant 11 makes WebJs a proper noun in prose and says a JSON description value is prose, and core, server and cli all already write it that way. Nothing caught it: the prose hook scans only new content, and the published-package brand test skips this manifest because the extension is private. Both strings reach users only through a publish, hence the version bump.
What
The VS Code extension is published. It is live as
WebJs.WebJsat 0.2.6, shows as WebJs, and carries the new monogram icon.
Before this, every install instruction we shipped pointed at
webjsdev.webjs, which returns 404. The docs site's editor-setup page is theone users actually follow, so editor setup was broken for anyone who tried it.
Closes #396 for the Marketplace half. Open VSX is still not published and
never was, under either identity, so the surfaces that promised it now say so
and send those editors to the
.vsixinstead of a second dead link.The non-breaking space in
displayNamedisplayNameisWebJsfollowed by U+00A0. It renders as plain "WebJs" andit is deliberate.
A listing briefly existed under the id
webjs-vscodeand was unpublished.The Marketplace retires an unpublished extension's id and its display
name permanently, so both of these are refused:
webjs-vscodeThe extension 'webjs-vscode' already exists in the MarketplaceWebJsThis extension display name is takenA plain trailing space does not work either, because the uniqueness check
trims ASCII whitespace before comparing. U+00A0 survives that trim. The id
has no equivalent escape hatch:
vscevalidates it locally against/^[a-z0-9][a-z0-9\-]*$/i.PUBLISHING.mdnow documents this so nobody "tidies" the character away andbreaks the next publish. The durable fix is to ask Microsoft to release the
retired name, then drop the character and the matching assertion.
Verification
.vsixwas downloaded from the Marketplace and comparedagainst the local build: all 13 entries identical.
webjs checkinwebsite/: all checks pass.webjsdev.webjsand the other stale tokens: clean.Not addressed
packages/editors/vscode/AGENTS.mdclaims a pre-commit gate enforces achangelog entry per version bump. No such gate exists; the hook only blocks
commits on
main. The entry here was written to the convention regardless.Flagging rather than fixing, since it predates this work.