Per-package, per-version release notes for @webjsdev/core,
/server,
/cli,
/intellisense,
/ui, and
/mcp,
plus the WebJs editor extensions for VS Code
(vscode) and Neovim
(nvim).
Each version-bump produces one entry, automatically.
cli
v0.10.50
3 changes
Features
serve the UI gallery at webjs.dev/ui (#1126) d9b2fe1d * refactor(website): extract the docs shell into lib/docs-shell.ts
The sidebar, mobile drawer, and .prose-docs typography move out of the docs sub-layout into a shared shell so the component library at /ui
teach HTTP verbs and GET caching on the gallery server-actions card (#1152) 5b6b67c1 * feat: teach HTTP verbs and GET caching on the server-actions card
The full-stack gallery used method = 'GET' incidentally in two query files and never showed cache, tags, or invalidates at all, so a
add asset() for content-hashed public urls510c32c8 * feat: add asset() for content-hashed public urls
An app's own asset urls sat at stable paths, so a CDN kept serving the previous bytes after a deploy. That caused two visible regressions on
server
v0.8.58
2 changes
Features
add asset() for content-hashed public urls510c32c8 * feat: add asset() for content-hashed public urls
An app's own asset urls sat at stable paths, so a CDN kept serving the previous bytes after a deploy. That caused two visible regressions on
Fixes
a partial fragment is never shared-cacheable (#1141) d9a21e8e * fix(server): a partial fragment is never shared-cacheable
Closes #1140
core
v0.7.46
3 changes
Features
add asset() for content-hashed public urls510c32c8 * feat: add asset() for content-hashed public urls
An app's own asset urls sat at stable paths, so a CDN kept serving the previous bytes after a deploy. That caused two visible regressions on
Fixes
stop SSR instantiating a component named inside a comment (#1132) 973309af * fix(core): stop SSR instantiating a component named inside a comment
The custom-element walk matched tags with a flat regex over assembled markup, so a registered tag name written inside an HTML comment was
refuse a function in form action=, it leaked server action source (#1167) 86605c21 * fix: refuse a function in form action=, it leaked server source
At SSR a 'use server' import resolves to the REAL function (the RPC stub exists only in the browser), and action= is an ordinary escaped attribute
vscode
v0.2.3
3 changes
Fixes
the "open documentation" command points at the current docs host (#1101) e07ea3a1
The documentation moved from docs.webjs.dev onto the main domain, and the command shipped in the extension still opened the old host. This is the extension-side counterpart of the same fix released in @webjsdev/core 0.7.44.
brand casing corrected in the Marketplace listing (#851) 28e5be9a
The extension README is packaged into the vsix and rendered as the Marketplace listing page, so this is the visible description text.
Also includes a JSDoc-only brand-casing pass over the extension source (#855) f41d105b, with no behaviour change.
nvim
v0.2.3
2 changes
Fixes
the vendored @webjsdev/intellisense bundle reported the wrong version (#1117)
The plugin ships a committed copy of @webjsdev/intellisense rather than resolving it at install time. That copy still declared 0.5.1 in its own package.json, so the published plugin misreported which language-service version it carried. Re-vendored, so the shipped bundle and its stated version agree.
There is no change to the plugin's own Lua surface. The only other commit touching this package since 0.2.2 was a JSDoc brand-casing pass inside the vendored copy (#855) f41d105b, with no behaviour change.
core
v0.7.45
1 change
Fixes
a stale hover prefetch no longer degrades a click to a full page load (#1115) f34fa5c9
Clicking a link could intermittently reload the whole document instead of soft-navigating, showing a loading spinner in the browser tab and flashing the entire page including preserved layout chrome.
A prefetched fragment is a reduced response, anchored at the deepest layout boundary the server short-circuited on, and it can only be applied to a DOM that still offers that boundary. Prefetching any page under a layout while the client already holds that layout therefore yields a fragment that applies from a sibling page but nowhere outside the section. Two ordinary interactions left one cached at the wrong moment: a hover's intent timer resolving after the click it belonged to had already swapped, and a hover on a link that was never clicked before navigating away. Consuming such an entry left the router nothing to swap into, so it correctly degraded to a full page load.
The prefetch cache now validates a fragment's anchor against the live boundaries and discards it when the anchor is gone, costing one network round-trip instead of a document load. Validation runs against the boundaries captured before the optimistic loading skeleton clears them, so an app with a root-level loading.{js,ts} keeps a usable prefetch cache. The router also no longer prefetches the page it is already on, which never helped a later navigation and only occupied a capped cache slot.
client-router degradations are now observable in production (#1115) f34fa5c9
When a soft navigation is not safely possible the router degrades to a full page load, which is correct but was previously reported only by a development-mode console warning, leaving deployed apps with no signal at all. Every such path now dispatches a webjs:navigation-fallback event on document in all environments, with detail { cause, href, willReload }. willReload is true only when a document load actually follows, so a listener can count real full loads without also counting the degradations that resolve in place (a dropped background revalidation, or a suppressed cross-deploy reload that proceeds on the stale importmap). The event is not cancelable, and a listener that throws cannot affect the navigation.
core
v0.7.44
1 change
Fixes
point the static properties error at webjs.dev/docs (#1101) e07ea3a1
The documentation moved from docs.webjs.dev onto the main domain, and the error thrown when a component declares its own static properties block still linked the old host. It is the only such URL in the package.
Released on its own rather than waiting for the next functional core change, because a URL baked into a thrown error is the one thing a published version can never take back: every install carries its link for as long as it runs. The old host redirects permanently, so nothing is broken either way, but the sooner the message is right the fewer installs carry a stale one.
server
v0.8.57
2 changes
Fixes
derive the request origin safely, and honor X-Forwarded-Proto / X-Forwarded-Host on Bun (#1091) 8ce50392
This affects both runtimes. Upgrade if your app derives any absolute URL.
On the node shell the origin was built by resolving the request target against the proxy headers, and four inputs a client controls could poison it. The first three applied to node only; the fourth to both runtimes:
- A request target beginning with // was read as an authority, not a path. GET //evil.com/x produced an origin of http://evil.comand silently rewrote the path to /x, so a different route matched than the one requested. - X-Forwarded-Proto was honored for any scheme. A value of javascript produced an origin of the literal string null, so every absolute URL the app derived became null/.... - A malformed X-Forwarded-Host (an unparseable authority, or an out-of-range port) threw Invalid URL, which surfaced as a 500 on the fetch path. - An absolute-form request line supplied the origin outright. GET http://evil.com/x HTTP/1.1 is reported verbatim as the request url by both node's HTTP server and Bun.serve, so it resolved to an origin of http://evil.com with the path rewritten to /x. An unproxied app was exposed to this directly.
The origin is now assembled by assigning the parts rather than resolving, the forwarded scheme is restricted to http and https, and an unparseable forwarded authority is ignored rather than raising.
Separately, on the Bun shell the forwarded headers were not honored at all, so behind a TLS-terminating proxy every absolute URL came out with an http:// origin. This shipped: webjs.dev runs on Bun behind Cloudflare and served <meta property="og:image" content="http://webjs.dev/public/og.png">. The blast radius is every absolute URL an app derives, so OAuth callbacks, canonical tags and sitemap entries were affected too, not just OG tags.
Both entry points now funnel through one readForwarded and one resolveOrigin, so the header and origin decisions cannot drift between the runtimes. What each shell RECEIVES still differs, and deliberately so: node gets an origin-form request target it treats as a path, Bun gets a url its own parser already resolved, so an absolute-form request line still routes to a different path on each. The origin, which is the security-relevant part, is decided in one place for both.
The correction is applied to the Request itself rather than threaded alongside it, because a route.ts handler reads req.url directly. The rebuild is skipped entirely when no forwarded header changes the origin, so an unproxied app keeps the no-clone hot path, and the WebSocket upgrade path gets the same treatment.
Embedding note: the forwarded correction belongs to startServer, which owns the listener. If you embed WebJs via createRequestHandler, you hand it the Request and it derives absolute URLs from that url, so you must rebuild the Request with the original scheme and host yourself. The same applies to the trusted client IP.
load a root middleware.ts, not only middleware.js (#1101) e07ea3a1
The root middleware lookup was the single literal middleware.js, so an app whose root middleware was written in TypeScript never had it loaded. There was no error and no warning: a missing middleware looks exactly like an app that has none, which is why this went unnoticed even though examples/blog ships a root middleware.ts that had never run.
TypeScript is the documented default for an app, the scaffold writes middleware.ts for the api template, and every other routing convention matches on the file stem. This was the one lookup that did not. It now resolves .ts, .js, .mts, and .mjs, with .ts first.
On upgrade, a root middleware that has never executed starts executing. That is the point of the fix, but it is a real behaviour change on a patch bump, so check what yours does before deploying. An app scaffolded with --template api ships a root middleware.ts applying cors() with a placeholder origin allow-list, which becomes live (including the OPTIONS preflight short-circuit); a hand-written one gating auth or issuing redirects goes from dormant to enforcing.
point the framework's own error messages at webjs.dev/docs (#1101) e07ea3a1
The documentation moved onto the main domain, and two thrown errors still linked the old host: the stub raised when a server-only module is imported from the browser, and the strip failure raised on non-erasable TypeScript. A URL baked into an error is the one thing a published version can never take back, so the sooner the message is right the fewer installs carry a stale one. The old host redirects permanently, so neither was broken.
cli
v0.10.49
1 change
Fixes
watch every root-middleware extension the server loads (#1101) e07ea3a1
webjs dev added a --watch-path for middleware.ts and middleware.js only. Paired with the server now resolving four extensions, an app with a root middleware.mts or middleware.mjs would have had its middleware loaded but never restarted the dev server when it was edited. Widening the watch list keeps the two in step.
A drift test now reads the server's own candidate list rather than restating it, so they cannot diverge again.
point the scaffold and CLI output at webjs.dev/docs (#1101) e07ea3a1
The documentation moved from docs.webjs.dev onto the main domain. A freshly scaffolded app carried the old host in the agent skill it ships (.agents/skills/webjs/, both SKILL.md and its references, the largest agent-facing surface in a generated app), its AGENTS.md, its agent rule file, the generated root-layout header nav, the generated home page footer, the gallery-clear script, and the tips printed after webjs create. The CLI's own unknown---template rejection message linked it too. The old host redirects permanently, so nothing was broken, but a new app should be generated with the current URL. The scaffold links are the bare form, and the old one chained two redirects (docs.webjs.dev to webjs.dev/docs to /docs/getting-started) where the new one costs a single hop.
The bundled skill also carries two content updates this release publishes: a line documenting the four root-middleware extensions the server accepts, and the reverse-proxy row in the Node vs Bun runtime comparison, now that the two shells agree on the forwarded headers.
ui
v0.3.10
2 changes
Features
Themed class-helper registry: refreshed utils and theme tokens (#1060) a35e1f52 * The registry's lib/utils.ts and themes/index.css were updated for the own-and-theme class-helper model (buttonClass / cardClass / inputClass and friends), so a copied primitive themes cleanly against the design tokens.
Fixes
tabs arrow-key nav moves focus, not just selection (#1079) e0c6996a * Arrow / Home / End on a focused tab now moves keyboard focus to the tab it selects, so the focus ring no longer strands on the previously focused tab (roving tabindex a11y).
cli
v0.10.48
1 change
Features
template-specific scaffold agent-docs with required app-building playbook (#1077) 4a1781f1 * chore: begin scaffold agent-docs context work
* feat(cli): template-specific scaffold AGENTS.md with required playbook
cli
v0.10.47
1 change
Fixes
resolve webjs ui bin via resolveBin, not the blocked exports subpath (#1074) fb7283e8
cli
v0.10.46
2 changes
Features
refactor the scaffold gallery onto a @webjsdev/ui class-helper design system (#1060) a35e1f52 * feat: ship @webjsdev/ui class-helper primitives into the gallery scaffold
Add components/ui/{button,card,input,textarea,badge}.ts (the ui tier-1 class helpers buttonClass/cardClass/inputClass/textareaClass/badgeClass) to the
Fixes
add cursor-pointer to the gallery streaming demo button (#1057) af638b94 The streaming demo's 'Stream tokens' button was the one gallery button missing cursor-pointer (Tailwind v4 dropped the default button cursor, and every other gallery button already sets it per-button). A full audit of every button and @click element in the gallery found this was the only gap.
server
v0.8.56
1 change
Features
refactor the scaffold gallery onto a @webjsdev/ui class-helper design system (#1060) a35e1f52 * feat: ship @webjsdev/ui class-helper primitives into the gallery scaffold
Add components/ui/{button,card,input,textarea,badge}.ts (the ui tier-1 class helpers buttonClass/cardClass/inputClass/textareaClass/badgeClass) to the
core
v0.7.43
2 changes
Fixes
view-transition soft-nav meta leak and stuck Suspense skeleton (#1049) e9af348b * fix: view-transition soft-nav meta leak and stuck Suspense skeleton
Two related client-router defects, both triggered by view transitions during a soft navigation.
preserve csp-nonce on mergeHead, warn on dropped streamed resolve (#1052) a32353e2 * fix: preserve csp-nonce on mergeHead, warn on dropped streamed resolve
#1050: mergeHead (the background full-body swap path) treated the live csp-nonce meta as stale whenever the incoming response carried a different
cli
v0.10.45
2 changes
Features
add streaming-action, view-transition, and webjs-suspense gallery demos (#1039) 7e523375 * feat: add streaming-action gallery demo (for await over an async generator)
A use-server action returning an async generator, consumed at the call site with for await so tokens render as they arrive. Fills a gallery gap:
fold auth into the default template as a gallery card, drop saas (#1040) 94b3b82a * chore: start auth gallery card work
correct core .d.ts type surface (phantom base + register param) (#1038) 2cb78130 * fix: stop core overlay leaking WebComponentBase as a value export
WebComponentBase is an internal base class the runtime never exports, but the .d.ts declared it as an abstract class. A class is a value as
server
v0.8.55
1 change
Features
Elision tokens track the native light-DOM slot surface (#1022) 842f9f84. Component elision drops the removed setSlotContent / hasSlot client-method signals and narrows the dynamic-slot detector to the native read surface (slotchange, assignedNodes / assignedElements / assignedSlot), so a display-only slotted wrapper stays elidable while a component that genuinely reads its slots still ships.
core
v0.7.41
2 changes
Breaking
The WebJs-specific light-DOM slot API is removed in favour of the native DOM slot API (#1022). setSlotContent(), hasSlot(), and the this.slots record no longer exist. Light DOM and shadow DOM now share one native surface: read slotted content with assignedNodes() / assignedElements() / assignedSlot / slotchange, and write it with appendChild / insertBefore / removeChild / el.slot = / innerHTML. Conditioning a render() on the slot record is gone. Use CSS :has() / slot:empty or a slotchange listener instead.
Features
Full light-DOM slot parity with shadow DOM (native API, one writer) (#1022) 842f9f84. Light-DOM <slot> now behaves identically to shadow DOM: post-mount native writes are live, the full read surface matches, and flipping static shadow changes nothing else. Built on a single-writer record model that ends the pre-#1016 three-observer bug lineage.
Fixes
Project forwarded-slot content and named-slot slices on the client (#1026) 4367b318. A template that forwards its slot into a nested component (html\<inner><slot></slot></inner>\`) now projects on client mount, not only during SSR (#1023), and a layout's ${children}` partitioned across multiple named slots now re-projects every named slice across a soft-nav swap, not just the default slice (#1024).
server
v0.8.54
1 change
Features
rebuild the client-router swap on route-keyed comment boundaries (#1016) 01b21276 * fix: full-load a soft nav fired while the document is still parsing
Phase 0 of the router/slot structural rebuild (#1013): the quick win.
core
v0.7.40
2 changes
Features
rebuild the client-router swap on route-keyed comment boundaries (#1016) 01b21276 * fix: full-load a soft nav fired while the document is still parsing
Phase 0 of the router/slot structural rebuild (#1013): the quick win.
Fixes
preserve comment markers when parsing a client-router navigation (#1010) 6cb3e8dc * fix: preserve comment markers when parsing a client-router navigation
Document.parseHTMLUnsafe strips every HTML comment in Chromium 150, and parseHTML routed every doctype'd response through it. The whole partial-swap
cli
v0.10.44
1 change
Features
add webjs-frame gallery demo and rework the server-actions demo (#989) 69b3e3f9 * feat: add webjs-frame partial-swap gallery demo
* fix: prune modules/frames in gallery:clear reset
core
v0.7.39
1 change
Fixes
recover a dropped wj:children close marker on soft nav (#994) 779dbb1d Recover an orphaned wj:children open marker whose close comment was dropped by the browser parser (a cross-engine parse race seen on Android Chrome and desktop Chromium), so a soft nav takes the correct scoped swap and the outer-layout navbar keeps its DOM identity instead of being wiped by the destructive full-body fallback. Bound the recovered range against the well-formed side's trailing-sibling count so a footer in the marker's own parent is preserved, and keep buildHaveHeader strict so an orphaned page fetches the full page where the bounding aligns.
mcp
v0.1.10
1 change
Features
local-first ui registry, on-demand example delivery, MCP ui tool (#984) 5e9be0b1 * chore: begin ui local-first registry work (#983)
* feat: resolve the ui registry local-first, deliver examples on demand
ui
v0.3.9
1 change
Features
local-first registry resolution + on-demand example delivery (#984) 5e9be0b1 * init / add / list / view resolve components from the packaged registry with no network round-trip; a custom --registry (or REGISTRY_URL) still fetches, and diff keeps comparing against the live upstream. * add copies a Tier-1 helper as a lean file (the class helpers plus a pointer) and strips the worked @example; the full structural example is served on demand by webjsui view and the new read-only MCP ui tool (one shared @webjsdev/ui/registry/extract projector). * diff now compares each local file against what add would write (import-rewrite plus example-strip), so a pristine install diffs clean. * init exits non-zero when the theme tokens cannot be written (previously a silent, unstyled install), and add self-heals missing tokens.
cli
v0.10.43
1 change
Features
local-first ui registry, on-demand example delivery, MCP ui tool (#984) 5e9be0b1 * chore: begin ui local-first registry work (#983)
* feat: resolve the ui registry local-first, deliver examples on demand
mcp
v0.1.9
1 change
Features
add webjs routes, doctor --json/--strict, per-command help (#979) c97159d6 * refactor: extract shared routes projector into @webjsdev/mcp
Pull the list_routes projection into a leaf routes-report.js module, mirroring check-report.js. The MCP list_routes tool now delegates to
cli
v0.10.42
1 change
Features
add webjs routes, doctor --json/--strict, per-command help (#979) c97159d6 * refactor: extract shared routes projector into @webjsdev/mcp
Pull the list_routes projection into a leaf routes-report.js module, mirroring check-report.js. The MCP list_routes tool now delegates to
server
v0.8.53
1 change
Fixes
reactive-props-no-class-field false positive on object-literal keys in a method (#980) f55eab75
mcp
v0.1.8
2 changes
Features
enforce WebJs brand casing with one simple prose rule + one-time fix (#855) f41d105b * feat: enforce WebJs brand casing with one simple prose rule
* docs: capitalize WebJs brand across markdown docs
re-skin the scaffold, single-source the agent skill, add gallery:clear (#971) 11f5f609 * feat: remove scaffold design-distinctness enforcement (#969)
The scaffold shipped six overlapping surfaces to force a generated app to look distinct from the scaffold (a blocking no-scaffold-placeholder check, a
intellisense
v0.5.4
2 changes
Features
enforce WebJs brand casing with one simple prose rule + one-time fix (#855) f41d105b * feat: enforce WebJs brand casing with one simple prose rule
* docs: capitalize WebJs brand across markdown docs
re-skin the scaffold, single-source the agent skill, add gallery:clear (#971) 11f5f609 * feat: remove scaffold design-distinctness enforcement (#969)
The scaffold shipped six overlapping surfaces to force a generated app to look distinct from the scaffold (a blocking no-scaffold-placeholder check, a
cli
v0.10.41
1 change
Features
re-skin the scaffold, single-source the agent skill, add gallery:clear (#971) 11f5f609 * feat: remove scaffold design-distinctness enforcement (#969)
The scaffold shipped six overlapping surfaces to force a generated app to look distinct from the scaffold (a blocking no-scaffold-placeholder check, a
server
v0.8.52
1 change
Features
re-skin the scaffold, single-source the agent skill, add gallery:clear (#971) 11f5f609 * feat: remove scaffold design-distinctness enforcement (#969)
The scaffold shipped six overlapping surfaces to force a generated app to look distinct from the scaffold (a blocking no-scaffold-placeholder check, a
core
v0.7.38
1 change
Features
re-skin the scaffold, single-source the agent skill, add gallery:clear (#971) 11f5f609 * feat: remove scaffold design-distinctness enforcement (#969)
The scaffold shipped six overlapping surfaces to force a generated app to look distinct from the scaffold (a blocking no-scaffold-placeholder check, a
cli
v0.10.40
3 changes
Features
replace static refresh with static interactive force-ship override (#965) ef23671d * feat: replace static refresh with static interactive force-ship override
The static refresh = true elision flag was a pre-seeding vestige. Its only mechanism was forcing a bare async component's module to ship, and
path-aware import-only elision for route modules (#964) 38ef79ae * feat: path-aware import-only elision for route modules
A client-effecting non-component module (the module-scope-signal idiom of invariant 5) reachable ONLY through shipping components no longer
regenerate stale dev build outputs on request so local CSS never goes stale (#970) d6cddfb9 * feat: regenerate stale dev build outputs on request (webjs.dev.regenerate)
* feat: scaffold uses dev.regenerate for Tailwind instead of a --watch task
server
v0.8.51
3 changes
Features
replace static refresh with static interactive force-ship override (#965) ef23671d * feat: replace static refresh with static interactive force-ship override
The static refresh = true elision flag was a pre-seeding vestige. Its only mechanism was forcing a bare async component's module to ship, and
path-aware import-only elision for route modules (#964) 38ef79ae * feat: path-aware import-only elision for route modules
A client-effecting non-component module (the module-scope-signal idiom of invariant 5) reachable ONLY through shipping components no longer
regenerate stale dev build outputs on request so local CSS never goes stale (#970) d6cddfb9 * feat: regenerate stale dev build outputs on request (webjs.dev.regenerate)
* feat: scaffold uses dev.regenerate for Tailwind instead of a --watch task
core
v0.7.37
2 changes
Features
replace static refresh with static interactive force-ship override (#965) ef23671d * feat: replace static refresh with static interactive force-ship override
The static refresh = true elision flag was a pre-seeding vestige. Its only mechanism was forcing a bare async component's module to ship, and
regenerate stale dev build outputs on request so local CSS never goes stale (#970) d6cddfb9 * feat: regenerate stale dev build outputs on request (webjs.dev.regenerate)
* feat: scaffold uses dev.regenerate for Tailwind instead of a --watch task
cli
v0.10.39
3 changes
Features
reduce first-feature scaffold friction (json column, db generate TTY, placeholder teardown) (#932) dd96d7fd * feat: add a json<T>() column helper to the scaffold db seam
Persisting a structured value (a board, a tag list, a settings blob) is one of the first things a real app needs, but the generated
Fixes
scaffold ships a static Tailwind stylesheet, not the browser runtime (#950) 6336d35d * fix: scaffold ships a static Tailwind stylesheet, not the browser runtime
A webjs create app rendered UNSTYLED with JavaScript disabled: the layout delivered Tailwind through the browser JIT runtime (tailwind-browser.js +
dogfood CLI DX papercuts (vendor pin, worktree resolve, prose hook) + engine recipe (#959) 1990dd60 * fix: prose hook allows a CLI subcommand before a closing quote
The block-prose-punctuation brand-casing rule treated a subcommand followed by a closing quote as lowercase brand prose, because its CLI
server
v0.8.50
2 changes
Features
reduce first-feature scaffold friction (json column, db generate TTY, placeholder teardown) (#932) dd96d7fd * feat: add a json<T>() column helper to the scaffold db seam
Persisting a structured value (a board, a tag list, a settings blob) is one of the first things a real app needs, but the generated
Fixes
dogfood CLI DX papercuts (vendor pin, worktree resolve, prose hook) + engine recipe (#959) 1990dd60 * fix: prose hook allows a CLI subcommand before a closing quote
The block-prose-punctuation brand-casing rule treated a subcommand followed by a closing quote as lowercase brand prose, because its CLI
core
v0.7.36
3 changes
Features
reduce first-feature scaffold friction (json column, db generate TTY, placeholder teardown) (#932) dd96d7fd * feat: add a json<T>() column helper to the scaffold db seam
Persisting a structured value (a board, a tag list, a settings blob) is one of the first things a real app needs, but the generated
Fixes
parse client-router partial-nav fragments in body context (#936) (#941) 7d3a5141 * fix: parse client-router partial-nav fragments in body context (#936)
A same-layout client nav receives an INNER fragment that begins with the <!--wj:children:/--> layout marker and carries no doctype or html. parseHTML
client router never strips stylesheets on a soft nav (#936) (#945) ffc63bc0 * fix: client router full-loads instead of a destructive body swap (#936)
On real Android Chrome a soft nav stripped the head CSS and the outer layout (navbar), cascading to a broken fixed-header offset on Back. Root cause: the
server
v0.8.49
3 changes
Performance
preload walk roots at the shipped module set, not raw route entries (#921) a345012f * perf: preload walk roots at the shipped module set, not raw route entries
deduplicatedPreloads (the app-module + component modulepreload walk) rooted at the raw [route.file, ...route.layouts] route entries, so it walked a
Fixes
capture a dynamic import() inside a template ${} hole (#919) e484202f * fix: capture a dynamic import() inside a template ${} hole
The module-graph edge scanner missed a dynamic import() whose string-literal specifier sits inside a template-literal ${} hole
stamp the trusted socket IP on a WebSocket upgrade (#778) (#923) bef996d7 A WS(ws, req) handler calling clientIp(req) read the spoofable inbound x-webjs-remote-ip header on BOTH runtimes: the upgrade path built the handler's Request by copying all inbound headers and never (a) stripped the inbound x-webjs-remote-ip copy nor (b) stamped the framework-trusted
cli
v0.10.38
1 change
Features
add saas logout control and login error feedback (#911) 8d8c3892 * feat(scaffold): add saas logout control and login error feedback
The generated saas app shipped no way to log out (signOut was exported but unused) and swallowed a failed login: wrong credentials bounced silently to
core
v0.7.35
2 changes
Fixes
client router re-projects slotted content of a reused component (#910) b77194f4 * test: add #908 acceptance test for slot re-projection on soft nav
* fix: re-project slotted content of a reused hydrated component on soft nav
re-project slot actual/fallback boundary transitions on soft nav (#914) e3538280 * test: add #912 acceptance tests for slot actual/fallback transitions
* fix: re-project slot actual/fallback boundary transitions via the slot runtime
core
v0.7.34
1 change
Fixes
client router must not reconcile into hydrated components (#907) c5c797f0 * fix: client router must not reconcile into hydrated components
A light-DOM component renders into its own host via the client renderer, which stashes the live template instance (lit-html parts holding direct
core
v0.7.33
2 changes
Fixes
first-class dev hot-reload, with no restart flash and watching outside the appDir (#893, #894) (#896) 4fc7446b * The dev watcher only followed the appDir, so an app reading content from a sibling directory (a repo-root blog/ beside the app) got no reload when that content changed. The watcher now also follows configured paths outside the appDir. * On Node, webjs dev re-execs under node --watch, which restarted the whole process on an app edit and dropped the open dev connection, so an edit needed a manual refresh. Hot reload is now applied in place without the restart flash.
reflect app and framework deploys on client navigation via content signals (#899) (#900) 9902b1bd * After a deploy, the client router kept serving its in-memory prefetch/snapshot copy of a page, so a soft navigation showed the pre-deploy version until a hard refresh. The router now detects a changed app or framework build via content signals and refetches instead of serving the stale snapshot.
server
v0.8.48
2 changes
Fixes
first-class dev hot-reload, with no restart flash and watching outside the appDir (#893, #894) (#896) 4fc7446b * The dev watcher only followed the appDir, so an app reading content from a sibling directory (a repo-root blog/ beside the app) got no reload when that content changed. The watcher now also follows configured paths outside the appDir. * On Node, webjs dev re-execs under node --watch, which restarted the whole process on an app edit and dropped the open dev connection, so an edit needed a manual refresh. Hot reload is now applied in place without the restart flash.
reflect app and framework deploys on client navigation via content signals (#899) (#900) 9902b1bd * After a deploy, the client router kept serving its in-memory prefetch/snapshot copy of a page, so a soft navigation showed the pre-deploy version until a hard refresh. The server now emits a content signal on its responses so the client refetches instead of serving the stale snapshot.
cli
v0.10.37
1 change
Features
close the scaffold coverage-gate deferred gaps with real demos (#859) (#901) 9df1af33 * Every @webjsdev/core and @webjsdev/server export and routing-convention stem that the scaffold teaching-coverage gate had marked deferred now ships a real, runnable, commented demo in the generated gallery (or an honest internal: reclassification). New surfaces the scaffold now teaches: the context API and Task, the remaining lit directives (guard, cache, templateContent, asyncAppend, asyncReplace), Suspense streaming, redirect, disable/enableClientRouter, richFetch with json() request accessors, renderStream, render / renderToString, cspNonce, a new sessions feature, cache eviction, file-store config, OAuth provider presets, per-action context/signal, sitemapIndex, the documented test helpers, and setOnError via a shipped instrumentation.ts. Also adds the root-only global-error / global-not-found boundaries and the icon / apple-icon / opengraph-image / twitter-image metadata routes.
server
v0.8.47
1 change
Fixes
multi-tab dev live-reload and no empty CSS on hot reload (#887, #891) (#890) 7a950d81 * The dev live-reload client opened one EventSource per tab, so on the HTTP/1.1 dev server a handful of open tabs exhausted the browser's ~6-connections-per-host cap and later tabs hung. It now shares one connection across all tabs via a SharedWorker (with a per-tab EventSource fallback), so tab count no longer touches the connection pool. * A hot reload that landed while an external watcher (tailwindcss --watch) was rewriting public/tailwind.css served the 0-byte mid-rewrite file, so the page painted unstyled. In dev, a just-modified 0-byte static read is now re-read briefly so the truncated content never reaches the browser. Both fixes are dev-only.
server
v0.8.46
1 change
Fixes
route(module) auto-applies an action's declared middleware and validate (#876) (#879) 72bb3611 * The route() REST adapter only ran the middleware / validate passed via its opts, so an action's declared export const middleware protected the RPC boundary but not the REST one (a function reference cannot reach its sibling config exports). * route() now also accepts the action's module namespace (import * as postActions from '...'; export const POST = route(postActions)) and auto-applies the declared middleware and validate, so a guard declared once beside the action protects both boundaries. The bare-function form is unchanged, and an explicit opts value still overrides the declared config.
cli
v0.10.36
4 changes
Features
close coverage-gate deferred gaps with real scaffold demos (#870) e62d7e41
add a tier-2 scaffold teaching-coverage gate for core exports (#861) 35d180d2
add a boundaries gallery demo and harden the scaffold-sync skill (#854) 5f7bd1f0
Fixes
fresh saas scaffold passes typecheck and axe out of the box (#877, #878) (#879) 72bb3611 * Rewrite the onBeforeCache import to #lib/utils/dom.ts (it kept a registry-relative ../lib/dom.ts and failed TS2307), and resolve AUTH_SECRET through a typed const with a production fail-fast guard (it was assigned as string | undefined to a required string). * Give every generated page exactly one <h1>, raise the gallery label text to full text-muted-foreground contrast, and add accessible names to the file-upload and ref-focus inputs, so a fresh app returns zero axe violations.
cli
v0.10.35
1 change
Features
close Next.js 16 file-routing parity gaps (#848) (#849) 09c416a7 * docs: clarify proxy/middleware and 'use server' in nextjs gotchas
Next 16 renamed middleware->proxy; webjs deliberately keeps middleware (Remix/Koa-style chainable per-segment). Document the distinction and
server
v0.8.45
1 change
Features
close Next.js 16 file-routing parity gaps (#848) (#849) 09c416a7 * docs: clarify proxy/middleware and 'use server' in nextjs gotchas
Next 16 renamed middleware->proxy; webjs deliberately keeps middleware (Remix/Koa-style chainable per-segment). Document the distinction and
core
v0.7.32
1 change
Features
close Next.js 16 file-routing parity gaps (#848) (#849) 09c416a7 * docs: clarify proxy/middleware and 'use server' in nextjs gotchas
Next 16 renamed middleware->proxy; webjs deliberately keeps middleware (Remix/Koa-style chainable per-segment). Document the distinction and
cli
v0.10.34
1 change
Fixes
four verified webjs iteration-loop gaps from the tic-tac-toe dogfood (#845) (#846) 34ba0a74 Ship the scaffold gitignore template as a non-dotfile so npm cannot strip it from the published tarball (a .gitignore was stripped at publish, so a generated app shipped without a .env ignore and a real .env became trackable). Also mark the generated layout's light/dark theme apparatus as one removable block so a custom single-theme palette is easy to adopt, and ship the Next.js and raw-text-interpolation gotchas guidance. Found dogfooding a tic-tac-toe app (#845).
server
v0.8.44
1 change
Fixes
four verified webjs iteration-loop gaps from the tic-tac-toe dogfood (#845) (#846) 34ba0a74 Add the no-interpolation-in-raw-text-element convention check. It flags a template interpolation (${...}) placed as a child of a <style> or <script> element in a component. The server renderer emits it but the client renderer drops the raw-text hole, so it paints at SSR then wipes to empty on hydrate. Scoped to components, so a page or layout interpolating a css result into <style> (which renders server-only) is not flagged. Found dogfooding a tic-tac-toe app (#845).
core
v0.7.31
1 change
Fixes
four verified webjs iteration-loop gaps from the tic-tac-toe dogfood (#845) (#846) 34ba0a74 * fix(core): rebuild a mixed attribute when only a later hole changes
A multi-hole attribute (class="a ${x} b ${y}") is anchored at its first hole with the remaining holes as noop parts, so updateInstance's per-hole dirty-check dropped a change confined to a later hole and the attribute went stale. Point every non-anchor member at the anchor so any hole change rebuilds the whole attribute. Found dogfooding a tic-tac-toe app (#845): a cell kept a stale class when only its second class hole changed.
cli
v0.10.33
1 change
Fixes
author the initial migration during scaffold so first run works out of the box (#843) 2c9184d5webjs create now runs webjs db generate after install, so the shipped example works on the first npm run dev and container boot with no manual database step.
mcp
v0.1.7
1 change
Fixes
source tool no longer reads as "not found" when nothing was searched (#838) e5eb00b5 The source({ query }) grep returned a misleading "no matches" when zero @webjsdev/* packages resolved (so nothing was actually searched); it now reports that distinctly, and a genuine no-match discloses the searched scope so absence is trustworthy (#837).
cli
v0.10.32
2 changes
Features
harden scaffold agent discipline (commits, worktrees, design, tailwind) (#836) 78d17879 * feat(cli): enforce commit-per-unit and auto-clean merged worktrees in the scaffold
* feat(cli): harden unique-design and tailwind-first scaffold guidance
Fixes
service-worker root assets (#830) + all tic-tac-toe dogfood findings (#837) (#838) e5eb00b5 * fix(server): serve service-worker root assets at the site root (#830)
* feat(cli): teach redirect-footgun, component browser tests, drizzle reads in the gallery
server
v0.8.43
1 change
Fixes
service-worker root assets (#830) + all tic-tac-toe dogfood findings (#837) (#838) e5eb00b5 * fix(server): serve service-worker root assets at the site root (#830)
* feat(cli): teach redirect-footgun, component browser tests, drizzle reads in the gallery
* fix(ui): split registry lib/utils.ts so importing cn() does not pin a page (#819)
ship an idiomatic feature gallery + example app in the scaffold (#826) 96b72150 * chore: start the scaffold feature-gallery (#824)
* feat(cli): gallery flagship /todo example (optimistic + a11y + PE + modules) (#824)
core
v0.7.30
1 change
Fixes
Correct the docs URL in the reactive-property error message (docs.webjs.com to docs.webjs.dev) (#826) 96b72150
ui
v0.3.7
7 changes
Features
webjs:before-cache hook; kit overlays reset on back/forward (#766) (#769) 4e569110 The client router keeps a Turbo-style outerHTML snapshot cache for instant back/forward. An overlay that is open when you navigate away is captured open and restored open on Forward (repro: open a hover-card, Back, Forward, still open). Add a webjs:before-cache event the router dispatches on document.
Fixes
dialog/alert-dialog invisible on iOS (viewport host, not 0x0) (#730) (#734) e0ba4d2f Root cause (confirmed on a real iPhone via /diag): the native <dialog> host was w-0 h-0. WebKit makes a top-layer <dialog> the containing block for its position:fixed descendants, so the fixed content panel (w-full) collapsed to 0x0 and the dialog was invisible on iOS, while Blink (Android/Chromium) resolves the
dialog/alert-dialog open via querySelector, not the iOS-null ref (#730) (#735) 6fd7ea3d v4 on a real iPhone: the dialog set open=true + locked scroll but the native <dialog> never opened (nativeOpen=n), while a direct showModal() worked (v3). The component reached the native <dialog> through this.#dialog.value (a ref) which came back NULL on iOS WebKit (the ref did not populate through SSR
assign tabs scope id in willUpdate so it serializes at SSR (#730) (#737) 75c2e5c5 The served HTML showed tab triggers with id=ui-tabs-25-trigger-* but the <ui-tabs> host had NO id: ensureId(this) ran lazily during a child trigger's render, AFTER the host's opening tag was serialized, so the host shipped no id. On hydration the client re-minted a DIFFERENT counter id, so the trigger/panel
hover-card, dropdown submenu, sonner on iOS/touch (#745) (#746) ea82838b * fix(ui): route sonner toast bus through globalThis, not module scope (#745)
The app registers <ui-sonner> from the version-hashed sonner.ts?v=<hash> while a caller may import('/components/ui/sonner.ts') bare. Those are distinct module
hover-card stays open on iOS touch; gate hover handlers (#745) (#758) 6b5cae46 The #746 fix added tap-to-open but left the mouseenter/mouseleave/focusin/ focusout handlers active. On a real iPhone, iOS Safari fires SYNTHETIC mouse/focus events around a tap, so _onEnter/_onLeave still ran and immediately re-closed the tap-opened card (the card opened then vanished; emulation does not
hover-card trigger tap never navigates on touch (no history pollution) (#745) (#760) 95398eed The #758 fix only preventDefault'd on the OPENING tap; a re-tap while the card was open fell through, so the inner <a>'s bubble-phase click reached the client router, which pushState()s when the event is not defaultPrevented. Each tap pushed a history entry, so Back needed N presses to leave the page. On touch the
cli
v0.10.30
3 changes
Fixes
address four dogfood issues (#791, #792, #793, #794) (#800) 3cb3c3db - #794: Fix block-comment-close bug in hello.test.ts template. The glob test/**/.test.ts closed the enclosing / */ comment early. Switched to single-line comments. - #793: Add no-redirect-in-api-route check rule. redirect() from
eliminate first-iteration failure traps for AI agents (#810) cb9001c7 * fix: stop no-server-import check flagging a type-only import (#805)
A TYPE-ONLY import type { Todo } from '#db/schema.server.ts' in a browser-shipped page or component is fully erased by the TS stripper, so
run real component tests under webjs test --browser (#806) (#812) b9defd4d * chore: start browser-test harness (#806)
add React/Next.js-style optimistic() state API to @webjsdev/core (#799) f7adcd94 * feat: add React/Next.js-style optimistic() state API to @webjsdev/core
* improve optimistic() JSDoc, remove dead controller registration, add declarative browser test
Fixes
eliminate first-iteration failure traps for AI agents (#810) cb9001c7 * fix: stop no-server-import check flagging a type-only import (#805)
A TYPE-ONLY import type { Todo } from '#db/schema.server.ts' in a browser-shipped page or component is fully erased by the TS stripper, so
Route precedence used a coarse 3-bucket score (static=1/dynamic=2/ catch-all=3) and same-bucket ties resolved by filesystem walk order, so
track string-literal dynamic import() in the browser graph (#767) 5add5d37 * fix: track string-literal dynamic import() in the browser graph
A dynamic import('./local.ts') of an app module was never discovered by the regex import scanner (it matched only static import/export-from), so
make the revalidateTag index atomic via store set ops (#768) 9b4f27f8 * fix: make the revalidateTag index atomic via store set ops
The tag -> cacheKey index was a read-modify-write of a JSON array, not atomic across processes: two concurrent mutations appending a key to the
core
v0.7.28
3 changes
Features
webjs:before-cache hook; kit overlays reset on back/forward (#766) (#769) 4e569110 The client router keeps a Turbo-style outerHTML snapshot cache for instant back/forward. An overlay that is open when you navigate away is captured open and restored open on Forward (repro: open a hover-card, Back, Forward, still open). Add a webjs:before-cache event the router dispatches on document
Fixes
valid hydration marker so slot components work on iOS (#730) (#744) 95550fc3 MARKER was 'w$' since the first commit. Part-sentinel attribute names are built as data-${MARKER}${i} -> data-w$0, and discoverSlots applies them via the strict setAttribute API. The '$' is not a valid attribute-name character: Chromium and desktop WebKit tolerate it, but iOS WebKit's setAttribute enforces
strip renamed wjm- hydration markers in ssrFixture normalize() (#762) 4a95966c #744 renamed the hydration marker from w$ to wjm- (a $ is invalid in an XML qualified name, which broke slot components on iOS), but the ssr-fixture browser test's normalize() still stripped only the old w$ form. The new <!--wjm-s-->/<!--wjm-0-->/<!--wjm-e--> markers survived into
cli
v0.10.29
1 change
Features
auto-apply migrations on dev (webjs.dev.before migrate) (#726) 030de282
Scaffolded apps add webjs db migrate as a webjs.dev.before step (mirroring start.before), so after you db:generate a migration, npm run dev applies it with no manual db:migrate. .env is now loaded before the dev before-steps too, so a Postgres dev migrate sees DATABASE_URL.
mcp
v0.1.6
1 change
Changes
resolve @webjsdev from node_modules, not the Bun cache (#722) 71f204e3
The source tool no longer falls back to resolving @webjsdev/* from Bun's global install cache (a Bun zero-install path); it resolves from node_modules like Node, which a Bun app now has.
core
v0.7.27
1 change
Changes
remove the webjs.pin config key (Bun zero-install) (#722) 71f204e3
The pin key is removed from the WebjsConfig type and the JSON schema, since the Bun zero-install pin shim it gated is gone (Bun installs like Node now).
server
v0.8.39
1 change
Changes
remove the Bun zero-install dependency pin shim (#722) 71f204e3
Drops the onLoad specifier-rewrite that pinned bare imports to package.json versions for Bun auto-install, and the importmap version-sharing that fed it. Bun resolves dependencies from node_modules now, the same as Node. The webjs.pin config key and the WEBJS_PIN env switch are removed. SSR action seeding (#472) on Bun is unaffected.
cli
v0.10.28
1 change
Changes
remove Bun zero-install: bun create now runs bun install like Node (#722) 71f204e3
Bun apps now install with bun install (a node_modules), the same as Node, rather than resolving deps on the fly. The scaffold no longer skips the install on Bun, drops the webjs-bun.mjs auto-install bootstrap (bun scripts are bun --bun webjs dev), and removes the cli's @webjsdev/* import-retry.
Migration: run bun install in a Bun app (the scaffold does this for you on bun create). Bun stays a first-class runtime.
server
v0.8.38
1 change
Fixes
keep server-only @webjsdev packages out of the browser vendor path (#716) bcd453ea * fix: keep server-only @webjsdev packages out of the browser vendor path
A fresh bun zero-install bun run dev logged could not vendor '@webjsdev/cli@^0.10.27/bin/webjs.js' via jspm and spent ~437ms on the failed
cli
v0.10.27
1 change
Fixes
pin the cli's @webjsdev/* imports under bun zero-install (#711) 5454ecb4 * fix: pin the cli's @webjsdev/* imports under bun zero-install (#709)
A fresh bun zero-install bun run dev failed: the cli (run from the global cache) does a bare import('@webjsdev/server'), and bun's runtime auto-install
cli
v0.10.26
1 change
Features
scaffold @webjsdev/* and pg as caret ranges, keep drizzle exact (#702) dce30f01 * feat: scaffold @webjsdev/* and pg as caret ranges, keep drizzle exact
#692 pinned the scaffold's deps exact because a bun zero-install range resolved to absolute latest. #698 fixed that (a normal caret resolves the
server
v0.8.37
3 changes
Features
forward inline-safe ranges in the bun zero-install pin rewrite (#698) 143c82a7 * feat: forward inline-safe ranges in the bun zero-install pin rewrite
Bun's runtime auto-install ignores package.json for a bare import (it fetches latest), so webjs rewrites a declared dep's specifier to an
share the server's resolved dep versions with the zero-install importmap (#701) a7a654da * feat: share the server's resolved dep versions with the zero-install importmap
Under Bun zero-install (no node_modules) getPackageVersion (require.resolve) finds nothing, so vendorImportMapEntries dropped the vendor entry and a
Fixes
don't forward a caret-prerelease range in the bun pin rewrite (#706) c6a20525 isInlineableVersion (#698) accepted a range operator combined with a prerelease suffix (^1.0.0-rc.3), which the rewrite then forwarded as an inline specifier. But bun zero-install ENOENTs on a caret-prerelease (verified: drizzle-orm@^1.0.0-rc.3 errors, the exact 1.0.0-rc.3 resolves),
cli
v0.10.25
3 changes
Features
pin Bun zero-install deps via an onLoad specifier-rewrite from package.json (#686) df2b03e8 * feat(server): add the bun zero-install specifier-pin transform core (#685)
The runtime-neutral core that rewrites bare import specifiers of declared deps to inline-versioned (name@version), so Bun auto-install fetches the pinned
bun create skips install by default (zero-install dev); node unchanged (#691) 24c5fb08 * feat(cli): bun create skips install by default (zero-install dev); node unchanged (#682)
Per-runtime create-time install default: Node installs (needs node_modules to run), Bun SKIPS (zero-install, 'bun run dev' resolves deps on the fly). A new
exact-pin scaffold deps so npm and bun resolve identical versions (#692) (#693) 236cecc4 A bun zero-install scaffold resolves deps to absolute-latest (ignoring ranges, #690), while npm install takes latest-in-range, so the two scaffolds diverged. Worse, drizzle-orm's npm 'latest' tag is a 0.x line while the scaffold targets the 1.0 relations-v2 RC, so bun pulled the WRONG major. Fix: exact-pin the
mcp
v0.1.5
1 change
Fixes
source tool resolves @webjsdev from the Bun cache under zero-install (#688) 8505e82b * fix(mcp): source tool resolves @webjsdev from Bun cache under zero-install (#687)
The source tool's resolveFrameworkRoots walked the require.resolve node_modules dirs, so under Bun zero-install (no node_modules, #675) it found nothing and
server
v0.8.36
1 change
Features
pin Bun zero-install deps via an onLoad specifier-rewrite from package.json (#686) df2b03e8 * feat(server): add the bun zero-install specifier-pin transform core (#685)
The runtime-neutral core that rewrites bare import specifiers of declared deps to inline-versioned (name@version), so Bun auto-install fetches the pinned
core
v0.7.26
1 change
Features
Add the webjs.pin config key to the WebjsConfig type (#686) df2b03e8
Declares the Bun zero-install version-pinning switch (webjs.pin, default true) in the typed config, so the config object accepts it. The pinning behavior itself ships in @webjsdev/server (#685).
vscode
v0.2.2
1 change
Features
bundled intellisense tracks custom prop attribute completions (#672) 6ebb044d * Republish: the extension bundles @webjsdev/intellisense, which now derives template attribute completions / hover from a reactive prop's custom attribute option (0.5.3). No extension-source change; the version bump ships the refreshed bundle.
nvim
v0.2.2
1 change
Features
bundled intellisense tracks custom prop attribute completions (#672) 6ebb044d * Republish: the plugin vendors @webjsdev/intellisense, which now derives template attribute completions / hover from a reactive prop's custom attribute option (0.5.3). The vendored copy was refreshed; this version bump ships it.
intellisense
v0.5.3
1 change
Features
derive template attribute completions from a prop's custom attribute option (#672) 6ebb044d * In-template attribute completions and hover now read a reactive prop's custom attribute option instead of always kebab-casing the property name, matching the runtime. A prop declared prop(Boolean, { attribute: 'is-open' }) completes is-open, not open.
cli
v0.10.24
3 changes
Features
remove better-sqlite3, scaffold uses the built-in SQLite (#670) 2613d909 * The scaffold no longer depends on the native better-sqlite3. SQLite uses the runtime's built-in driver via Drizzle: node:sqlite on Node, bun:sqlite on Bun. No native module build, no trustedDependencies.
zero-install dev/start on Bun via an auto-install bootstrap (#676) b3ffb3c2 * A Bun scaffold's dev / start / db scripts (and the start.before migrate) run through a generated webjs-bun.mjs bootstrap under bun --bun, so Bun auto-install resolves deps on demand and a fresh app serves with no bun install (install becomes optional, for editor types / offline). Node path unchanged.
Fixes
set busy_timeout + WAL on the SQLite connection to stop "database is locked" (#674) 55b9b369 * node:sqlite and bun:sqlite both default busy_timeout to 0, so a contended write threw database is locked immediately (better-sqlite3 used 5000ms). The generated connection now sets PRAGMA busy_timeout = 5000 + WAL.
server
v0.8.35
1 change
Features
resolve a custom prop attribute to its property during SSR (#672) 6ebb044d * applyAttrsToInstance (the SSR path) now honors a reactive prop's custom attribute option, so a parent-rendered <el is-open> gets the right property value in the JS-off first paint, matching the client. Part of implementing the previously-documented-but-unimplemented default + custom attribute prop options.
core
v0.7.25
1 change
Features
implement the reactive-prop default and custom attribute options (#672) 6ebb044d * Both were documented but never consumed. default now sets a prop's initial value (a value, or a function called per instance for fresh objects/arrays; a constructor assignment still overrides it). A custom attribute name is now honored in observedAttributes, reflection, and attributeChangedCallback (lit-parity). Works for both the bare { count: Number } and prop(Number, {...}) declaration forms.
cli
v0.10.23
2 changes
Features
Origin/Sec-Fetch-Site CSRF check so SSR pages are CDN-cacheable (#660) 453d072e * feat(server): Origin/Sec-Fetch-Site CSRF check, drop the token cookie
Replace the per-request webjs_csrf double-submit cookie with a cross-origin check on state-changing action requests: Sec-Fetch-Site
advisory naming why a page or layout ships (the elision blocker) (#666) 00f12fca * chore: start elision-blocker advisory (#646)
server
v0.8.34
2 changes
Features
Origin/Sec-Fetch-Site CSRF check so SSR pages are CDN-cacheable (#660) 453d072e * feat(server): Origin/Sec-Fetch-Site CSRF check, drop the token cookie
Replace the per-request webjs_csrf double-submit cookie with a cross-origin check on state-changing action requests: Sec-Fetch-Site
advisory naming why a page or layout ships (the elision blocker) (#666) 00f12fca * chore: start elision-blocker advisory (#646)
core
v0.7.24
1 change
Features
Origin/Sec-Fetch-Site CSRF check so SSR pages are CDN-cacheable (#660) 453d072e * feat(server): Origin/Sec-Fetch-Site CSRF check, drop the token cookie
Replace the per-request webjs_csrf double-submit cookie with a cross-origin check on state-changing action requests: Sec-Fetch-Site
cli
v0.10.22
5 changes
Features
block raw custom elements extending HTMLElement with pretooluse hookc1363eed
client router auto-enables from the bundle, drop layout imports (#628) dbaf3f43 * feat: drop redundant client-router import from app layouts
The client router auto-enables when @webjsdev/core loads in the browser (router-client.js calls enableClientRouter() at module end, and the
flag array-typed reactive props declared with Object (#632) 257035d2 * feat(check): flag array-typed reactive props declared with Object
An array-typed reactive prop (prop<Tag[]>(...)) should pass the Array runtime constructor, not Object. The two share one converter in
Fixes
drop dead customElements helpers from blog cn.ts so page elides (#630) 6688c92a * fix: drop dead customElements helpers from blog cn.ts so page elides
examples/blog/lib/utils/cn.ts mixed a pure class-name merger with dead custom-element helpers (Base, defineElement, ServerHTMLElementStub) that
use position:fixed for pinned headers (scaffold + website) + document (#610) (#648) d450352d * fix: use position:fixed (not sticky) for pinned headers across apps + docs
Propagate the #610 fix beyond the blog. A position:sticky header flickers on iOS WebKit during a client-router nav; the fix is position:fixed with a
server
v0.8.33
6 changes
Features
elide page/layout modules that only import components (#609) c89183b8 * feat: elide page/layout modules that only import components
Pages and layouts never hydrate, yet the boot script imported the page module and every layout module on the client. For a typical route their only client
flag array-typed reactive props declared with Object (#632) 257035d2 * feat(check): flag array-typed reactive props declared with Object
An array-typed reactive prop (prop<Tag[]>(...)) should pass the Array runtime constructor, not Object. The two share one converter in
add webjs.clientRouter:false opt-out for the automatic client router (#643) 2e451763 * feat: add webjs.clientRouter:false opt-out for the automatic client router
#620 made the client router auto-enable whenever @webjsdev/core loads in the browser (any page with a component), removing the only de-facto opt-out an app
Fixes
elide display-only components in the declare-free factory DX (#608) 39111762 * fix: elide display-only components in the declare-free factory DX
A component written in the enforced factory form extends WebComponent({ ... }) was never elided, even when display-only:
stop elision false-positives that pin page/layout modules (#625) 79cb5bbc * fix: stop elision false-positives that pin page/layout modules
Pages and layouts never hydrate, so the analyser can drop their modules (inert #179 / import-only #605). In practice almost none qualified because
* fix: support backticks in scanner/elision, preserve comment delimiters, and add tests
core
v0.7.23
6 changes
Features
warn in dev when scroll-behavior: smooth is set on <html> (#614) a7653b36 * feat: warn in dev when scroll-behavior: smooth is set on <html>
The client router forces an instant scroll-to-top on a forward nav (matching a native page load), so an app-level scroll-behavior: smooth
client router auto-enables from the bundle, drop layout imports (#628) dbaf3f43 * feat: drop redundant client-router import from app layouts
The client router auto-enables when @webjsdev/core loads in the browser (router-client.js calls enableClientRouter() at module end, and the
add webjs.clientRouter:false opt-out for the automatic client router (#643) 2e451763 * feat: add webjs.clientRouter:false opt-out for the automatic client router
#620 made the client router auto-enable whenever @webjsdev/core loads in the browser (any page with a component), removing the only de-facto opt-out an app
Fixes
force instant scroll on navigation so smooth CSS does not animate it (#603) 5761e772 * fix: force instant scroll on nav so smooth CSS does not animate it
The client router restored scroll with the 2-arg scrollTo(x, y) form, which honors an app's html { scroll-behavior: smooth }. That made every
gate data-navigating to kill iOS nav flash, revert app-CSS attempts (#610) (#624) e7d9ba04 * fix: make the data-navigating loading hook opt-in to kill the iOS nav flash
The client router wrote data-navigating on <html> on every nav past the 150ms defer. Toggling an attribute on the root re-runs global style
use position:fixed for the blog header to end the iOS nav flicker (#610) (#640) 02fbb845 On-device isolation proved the root cause: a position:sticky header flickers its background for one frame on iOS WebKit (both Safari and Chrome on iOS, all WebKit) during a client-router forward nav. The post-swap scroll-to-top drives a sticky stuck-to-static recompute that WebKit mis-repaints. It is iOS-only
ui
v0.3.6
1 change
Features
100% accessibility out of the box + AI-agent a11y contracts (#656) 8f193aca * Tier-2 custom elements now wire their own WAI-ARIA pattern with zero author effort: tabs cross-links triggers and panels (aria-controls / aria-labelledby), reports aria-orientation, and marks an inactive panel inert; toggle-group uses a roving tabindex with Arrow / Home / End; dropdown-menu declares aria-orientation, reflects aria-disabled, and gives the trigger aria-haspopup / aria-expanded / aria-controls; dialog and alert-dialog name themselves from their title and description on open; tooltip references its tip via aria-describedby; hover-card exposes the popup relationship on its (focus-openable) trigger; sonner is a persistent aria-live region. * Tier-1 class helpers gained an A11y (required for accessible output) JSDoc block stating exactly the markup and ARIA the caller must supply.
Fixes
declare sibling-component registry dependencies so webjs ui add copies them (#656) 8f193aca * The add resolver walks registryDependencies only (it does not follow relative imports, apart from the special-cased ../lib/utils.ts rewrite). dialog, dropdown-menu, hover-card, tooltip, and toggle-group imported a sibling component without declaring it, so webjs ui add copied a file with a broken import. All five are declared now, with a guard test that fails on any future sibling-import gap.
nvim
v0.2.1
3 changes
Features
flag duplicate custom-element tag registrations (check rule + editor 9004) (#444) 5ff76b63 * feat: add no-duplicate-tag webjs check rule (#364)
* feat: flag duplicate custom-element tag registrations in editor (#364)
declare-free reactive-prop DX via dual-role WebComponent (#597) 2b409c51 * feat: implement declare-free reactive properties via factory
Introduce a callable WebComponent constructor that acts as a class factory and can be invoked with a property shape object. This allows properties to be fully typed without needing a manual declare line, while keeping full backward compatibility with the existing static properties field structure.
Reactive properties must now be declared via the extends WebComponent({…}) factory. A hand-written static properties in a class body throws at
intellisense
v0.5.2
2 changes
Features
declare-free reactive-prop DX via dual-role WebComponent (#597) 2b409c51 * feat: implement declare-free reactive properties via factory
Introduce a callable WebComponent constructor that acts as a class factory and can be invoked with a property shape object. This allows properties to be fully typed without needing a manual declare line, while keeping full backward compatibility with the existing static properties field structure.
Concurrent agents sharing one checkout collide: a git checkout in one moves HEAD under another, so commits land on the wrong branch (#590; a chore: release
declare-free reactive-prop DX via dual-role WebComponent (#597) 2b409c51 * feat: implement declare-free reactive properties via factory
Introduce a callable WebComponent constructor that acts as a class factory and can be invoked with a property shape object. This allows properties to be fully typed without needing a manual declare line, while keeping full backward compatibility with the existing static properties field structure.
pure oven/bun:1 scaffold Dockerfile (bun runtime) (#596) 60c46e1b * feat: bun scaffold Dockerfile is now a pure oven/bun:1 base
Now that @webjsdev/cli@0.10.20 (#570, npx-free webjs db/test) is the published latest, a freshly scaffolded bun app's boot-time webjs db migrate runs under
Reactive properties must now be declared via the extends WebComponent({…}) factory. A hand-written static properties in a class body throws at
server
v0.8.32
2 changes
Features
declare-free reactive-prop DX via dual-role WebComponent (#597) 2b409c51 * feat: implement declare-free reactive properties via factory
Introduce a callable WebComponent constructor that acts as a class factory and can be invoked with a property shape object. This allows properties to be fully typed without needing a manual declare line, while keeping full backward compatibility with the existing static properties field structure.
Reactive properties must now be declared via the extends WebComponent({…}) factory. A hand-written static properties in a class body throws at
core
v0.7.22
5 changes
Features
unify webjs dev/start/db with npm-script behavior via a tasks config (#554) f22ac7d3 * feat: orchestrate dev/start tasks from the webjs config block (#550)
* feat: add dev.before + scaffold tasks config, route db scripts via webjs db (#550)
switch the default ORM from Prisma to Drizzle (#558) 5938ab67 * chore: remove orphaned prisma-preflight (dead since #550, Prisma leaving in #551)
* feat: point webjs db at drizzle-kit instead of prisma
declare-free reactive-prop DX via dual-role WebComponent (#597) 2b409c51 * feat: implement declare-free reactive properties via factory
Introduce a callable WebComponent constructor that acts as a class factory and can be invoked with a property shape object. This allows properties to be fully typed without needing a manual declare line, while keeping full backward compatibility with the existing static properties field structure.
device-adaptive link prefetch, viewport-safe on mobile (#594) c6eae7ba * feat: device-adaptive link prefetch, viewport-safe on mobile
The client router's default prefetch was always 'intent' (hover/focus/ touch-start). On touch there is no hover and touchstart fires at tap
Reactive properties must now be declared via the extends WebComponent({…}) factory. A hand-written static properties in a class body throws at
cli
v0.10.20
2 changes
Features
make webjs db/test commands runtime-native (Bun-safe, no npx) (#571) 026710aa * feat: webjs db resolves drizzle-kit + spawns with the runtime (drop npx)
webjs db <generate|migrate|push|studio> shelled 'npx drizzle-kit', which fails in a pure oven/bun image (no npx). Resolve the app's drizzle-kit bin via its
Bun-first scaffold mode (--runtime bun) across all 3 templates (#569) 92d544cb * feat: --runtime bun scaffold mode (core plumbing + deploy/CI/docs rewrites)
Add a runtime axis orthogonal to --template (the 3-templates invariant is untouched). webjs create --runtime bun (and bun create webjs, auto-detected
switch the default ORM from Prisma to Drizzle (#558) 5938ab67 * chore: remove orphaned prisma-preflight (dead since #550, Prisma leaving in #551)
* feat: point webjs db at drizzle-kit instead of prisma
cli
v0.10.18
3 changes
Features
make SSR action-result seeding work on Bun (#529) (#534) e2c7c7a1 * feat: install SSR action seeding on Bun via Bun.plugin
Seeding (#472) rode Node's module.registerHooks, which Bun lacks, so it was off on Bun and every shipping async-render component re-fetched on hydration. Extract
unify webjs dev/start/db with npm-script behavior via a tasks config (#554) f22ac7d3 * feat: orchestrate dev/start tasks from the webjs config block (#550)
* feat: add dev.before + scaffold tasks config, route db scripts via webjs db (#550)
switch the default ORM from Prisma to Drizzle (#558) 5938ab67 * chore: remove orphaned prisma-preflight (dead since #550, Prisma leaving in #551)
* feat: point webjs db at drizzle-kit instead of prisma
server
v0.8.30
2 changes
Features
unify webjs dev/start/db with npm-script behavior via a tasks config (#554) f22ac7d3 * feat: orchestrate dev/start tasks from the webjs config block (#550)
* feat: add dev.before + scaffold tasks config, route db scripts via webjs db (#550)
switch the default ORM from Prisma to Drizzle (#558) 5938ab67 * chore: remove orphaned prisma-preflight (dead since #550, Prisma leaving in #551)
* feat: point webjs db at drizzle-kit instead of prisma
server
v0.8.29
3 changes
Features
make SSR action-result seeding work on Bun (#529) (#534) e2c7c7a1 * feat: install SSR action seeding on Bun via Bun.plugin
Seeding (#472) rode Node's module.registerHooks, which Bun lacks, so it was off on Bun and every shipping async-render component re-fetched on hydration. Extract
Fixes
hot-reload webjs dev under Bun via bun --hot (#514) (#519) 5aceb8b0 * fix: hot-reload webjs dev under bun via bun --hot (#514)
On Bun, webjs dev re-exec'd under node --watch (a Node-only flag) and relied on the dev re-import's ?t= cache-bust query. Bun keys its module cache by path
make the seed facade fail-open on an export-enumeration miss (#538) d5f545f5 * fix: make the seed facade fail-open on an export-enumeration miss
The facade enumerates a 'use server' module's named exports with a regex (extractExportNames) to emit one wrapped export const NAME per export. A
cli
v0.10.17
1 change
Fixes
hot-reload webjs dev under Bun via bun --hot (#514) (#519) 5aceb8b0 * fix: hot-reload webjs dev under bun via bun --hot (#514)
On Bun, webjs dev re-exec'd under node --watch (a Node-only flag) and relied on the dev re-import's ?t= cache-bust query. Bun keys its module cache by path
cli
v0.10.16
2 changes
Scaffold templates
The scaffold templates shipped into every new app gain Bun coverage and shed stale claims:
Bun runtime guidance. The scaffolded AGENTS.md documents running an app on Bun (bun --bun run dev / start) and that TypeScript stripping comes from amaro there (#510) 2c6d9599.
Corrected runtime framing (#515) ed558949: the scaffold Dockerfile no longer claims a removed esbuild fallback (webjs is buildless; node:24-alpine default with an oven/bun option), and the AGENTS.md invariant + CONVENTIONS.md code-style section now say types are stripped at the runtime layer (Node's built-in or amaro on Bun) rather than Node-only.
server
v0.8.28
4 changes
Features
Brotli compression on the Bun listener via node:zlib (#518) b905b34f * The Bun.serve shell compressed via the web CompressionStream (gzip only); it now uses node:zlib (native on Bun) for brotli on Bun, reaching full compression parity with the Node shell. The negotiation (br > gzip > deflate) + compressor factory are shared in listener-core.js so the two shells compress identically, and the body is bridged through a reader-loop generator + pipeline so a mid-stream error tears the chain down instead of hanging.
run on both Bun and Node via a pluggable TS stripper (#510) 2c6d9599 * feat: make the TS stripper pluggable so webjs runs on Bun and Node (#508)
* test: cross-runtime smoke (SSR + TS strip + action RPC) for Node and Bun (#508)
startServer now detects the host runtime and selects a listener shell: the existing node:http path on Node, a new Bun.serve path on Bun. Bun is
Bun test matrix in CI, fixing the cross-runtime bugs it found (#515) ed558949 * fix: normalize amaro's non-erasable TS error code to Node's (#509)
The Bun test matrix surfaced a genuine cross-runtime bug: the dev-error overlay classifies a TypeScript strip failure by err.code ===
core
v0.7.21
1 change
Fixes
The Bun test matrix (#515) surfaced two genuine cross-runtime serializer / SSR bugs in core, now fixed:
Serialize FormData reliably on Bun. Bun returns a fresh Blob/File identity on every FormData.entries() / get() call (Node returns a stable one), so the serializer's two-pass design (precompute Blob bytes by identity, then re-iterate to encode) missed its identity-keyed lookup and threw undefined is not an object (u8.buffer). Each FormData's entries are now captured once and reused across both passes, so a FormData with mixed string + Blob entries round-trips on every runtime. ed558949
Name the offending browser member in the SSR error hint on Bun.browserMemberHint anchored its TypeError / ReferenceError regexes to end-of-string, but JSC (Bun) appends a detail clause that V8 (Node) does not, so the actionable "this is a browser-only API touched during SSR" hint never fired on Bun. The match is now on a word boundary, so the hint fires on both engines. ed558949
server
v0.8.27
1 change
Features
flag a 'use server' file that exports no callable action (#506) c74f63db * feat: flag a 'use server' file that exports no callable action (#464)
* docs: document the use-server-exports-callable check rule (#464)
mcp
v0.1.2
1 change
Features
report action verb/cache/config in MCP list_actions (#501) 9f2e5418 * feat: report action verb/cache/config in MCP list_actions (#488)
Add extractActionConfig (lexical, no module load) to mcp.js that reads the reserved config exports (method, cache, tags, invalidates, validate,
cli
v0.10.15
2 changes
Features
async render() + component-level Suspense (bare-await data fetch) (#470) 5443ec9a * feat(server): treat async render() components as interactive in elision
An async (promise-returning) render() suspends on the client: it awaits data, re-renders with the resolved value, reads the SSR seed, and may
elide bare async-render components (#474) (#480) 63ca566d * feat(elision): elide bare async-render components
#470 made async render() a blanket interactivity signal, shipping every async component plus a redundant on-hydration re-fetch. A BARE async leaf
server
v0.8.26
2 changes
Breaking
REST endpoints move from expose() to route.ts; new route() adapter (#503) c0561758 The REST-via-expose() machinery is removed (expose() / validateInput() / getExposed() are gone from @webjsdev/core). A REST endpoint is now a route.ts that imports and calls a 'use server' action. The new route(action, opts?) export from @webjsdev/server is the optional one-liner for that pattern: it merges the URL query, route params, and JSON body into one input, runs an optional { validate } boundary validator, dispatches through the request AbortSignal and per-action middleware, and JSON-responds the result (a returned Response passes through). The per-action validator is the export const validate config export. buildActionIndex no longer loads any module.
Features
streaming RPC results (return a ReadableStream/async-iterable from an action) (#499) b82ce614 * feat: stream a ReadableStream/async-iterable action result over RPC (#489)
An action that returns a ReadableStream, async iterable, async generator, or Node Readable now streams its chunks over the single RPC response
Fixes
count a type-annotated arrow action in the one-action check rule (#495) (#500) 81b7cd7a The one-action-per-configured-file rule's arrow matcher did not allow a : Type annotation between the const name and the =, so an annotated arrow action (export const getA: Handler = (id) => ...) slipped past the lint, letting a configured file ship two callable actions. Broaden the
core
v0.7.20
2 changes
Breaking
remove expose() / validateInput() / getExposed(); REST endpoints move to route.ts (#503) c0561758expose(), validateInput(), and getExposed() are removed from @webjsdev/core. A server action stays a plain 'use server' function; to reach it over plain HTTP, put it behind a route.ts handler (import the action and call it), optionally via the new route() adapter from @webjsdev/server. The per-action validator is the export const validate config export. webjs has no users yet, so there is no deprecation shim.
Features
streaming RPC results (return a ReadableStream/async-iterable from an action) (#499) b82ce614 * feat: stream a ReadableStream/async-iterable action result over RPC (#489)
An action that returns a ReadableStream, async iterable, async generator, or Node Readable now streams its chunks over the single RPC response
add opt-in compile-time serializability types for actions (#488) (#502) 20a99175 A server action's args/return cross the RPC wire through the webjs serializer, which drops functions and a class instance's methods silently. Serializable<T> maps a fully serializable type to itself and a function / method position to a branded NonSerializable marker; SerializableArgs /
server
v0.8.25
3 changes
Features
HTTP-verb server actions via config exports (#488) (#494) 2785cd90 * feat: action config-export reader for HTTP-verb actions (#488)
The foundation: read the reserved sibling config exports (method/cache/ tags/invalidates/validate) from a 'use server' action module, with verb
AbortSignal cancellation for RPC, wired to async-render supersede (#492) (#496) fcfcfe5f * feat: AbortSignal cancellation for RPC, wired to async-render supersede (#492)
Server: the RPC endpoint runs the action inside runWithActionSignal(req.signal), so an action reads the request's AbortSignal via actionSignal() (from
per-action middleware with shared context (#490) (#497) fdcf056d * feat: per-action middleware with shared context (#490)
A 'use server' action declares export const middleware = [mw1, mw2], each async (ctx, next) => result, run around the action on BOTH the RPC endpoint
core
v0.7.19
3 changes
Features
HTTP-verb server actions via config exports (#488) (#494) 2785cd90 * feat: action config-export reader for HTTP-verb actions (#488)
The foundation: read the reserved sibling config exports (method/cache/ tags/invalidates/validate) from a 'use server' action module, with verb
AbortSignal cancellation for RPC, wired to async-render supersede (#492) (#496) fcfcfe5f * feat: AbortSignal cancellation for RPC, wired to async-render supersede (#492)
Server: the RPC endpoint runs the action inside runWithActionSignal(req.signal), so an action reads the request's AbortSignal via actionSignal() (from
Fixes
harden the wire deserializer against prototype pollution (#493) ea1b88a3 * fix: harden the wire deserializer against prototype pollution (#491)
JSON.parse turns a literal "__proto__" wire key into an own property, so the plain-object decode loop's out[key]=value invoked the prototype setter
server
v0.8.24
1 change
Features
seed SSR action results into hydration so async render does not re-fetch (#472) (#486) 86bd5a5a * feat: add SSR action-seed capture + client consumer modules (#472)
The server module installs a synchronous module.registerHooks facade that wraps each 'use server' export in a Proxy recording (file, fn, args) ->
core
v0.7.18
1 change
Features
seed SSR action results into hydration so async render does not re-fetch (#472) (#486) 86bd5a5a * feat: add SSR action-seed capture + client consumer modules (#472)
The server module installs a synchronous module.registerHooks facade that wraps each 'use server' export in a Proxy recording (file, fn, args) ->
server
v0.8.23
2 changes
Fixes
silence streamed Suspense boundary error message in prod (#482) ff84dd63 * fix: silence streamed Suspense boundary error message in prod
The page-level streaming path (streamingHtmlResponse) emitted the raw boundary error message in BOTH dev and prod, leaking internal detail (a
SSR error prod-silence keys on the dev flag, not NODE_ENV (#484) e0d27191 * fix: SSR error prod-silence keys on the dev flag, not NODE_ENV
defaultSSRErrorTemplate (the per-component SSR error isolation) and the renderToStream boundary catch gated prod-silence on NODE_ENV. But webjs
core
v0.7.17
1 change
Fixes
SSR error prod-silence keys on the dev flag, not NODE_ENV (#484) e0d27191 * fix: SSR error prod-silence keys on the dev flag, not NODE_ENV
defaultSSRErrorTemplate (the per-component SSR error isolation) and the renderToStream boundary catch gated prod-silence on NODE_ENV. But webjs
mcp
v0.1.1
2 changes
Features
extract the MCP server into a standalone @webjsdev/mcp package (#417) b4c86b67 * feat: extract the MCP server into a standalone @webjsdev/mcp package (#415)
The MCP server lived inside @webjsdev/cli (lib/mcp.js + mcp-docs.js + mcp-source.js + check-json.js), reachable ONLY as webjs mcp. Following
async render() + component-level Suspense (bare-await data fetch) (#470) 5443ec9a * feat(server): treat async render() components as interactive in elision
An async (promise-returning) render() suspends on the client: it awaits data, re-renders with the resolved value, reads the SSR seed, and may
server
v0.8.22
2 changes
Features
async render() + component-level Suspense (bare-await data fetch) (#470) 5443ec9a * feat(server): treat async render() components as interactive in elision
An async (promise-returning) render() suspends on the client: it awaits data, re-renders with the resolved value, reads the SSR seed, and may
elide bare async-render components (#474) (#480) 63ca566d * feat(elision): elide bare async-render components
#470 made async render() a blanket interactivity signal, shipping every async component plus a redundant on-hydration re-fetch. A BARE async leaf
core
v0.7.16
2 changes
Features
async render() + component-level Suspense (bare-await data fetch) (#470) 5443ec9a * feat(server): treat async render() components as interactive in elision
An async (promise-returning) render() suspends on the client: it awaits data, re-renders with the resolved value, reads the SSR seed, and may
elide bare async-render components (#474) (#480) 63ca566d * feat(elision): elide bare async-render components
#470 made async render() a blanket interactivity signal, shipping every async component plus a redundant on-hydration re-fetch. A BARE async leaf
cli
v0.10.14
4 changes
Features
convention-pick redirect() status + hint bare webjs dev's missing prisma client (#456) 5d5cd76b * feat: convention-pick redirect() status (302 GET gate, 307 action)
redirect('/login') baked in 307 unconditionally, so a GET-to-GET gate (an auth bounce) returned the method-preserving 307 where 302 Found is
make the opt-in webjs vendor pin output committable (#455) 3e125a1f * feat: make committable the opt-in webjs vendor pin output
Vendoring is an optional opt-in: the no-build default resolves bare specifiers at runtime via jspm.io and commits nothing. But when a user
flag an incoherent importmap dependency graph in webjs doctor (#460) 06c10efa * feat(server): importmap coherence check over the resolved dep graph
Add checkImportmapCoherence: given a produced importmap, for each resolved package it verifies that the version pinned for every OTHER resolved package it
Fixes
load .env before resolving the dev/start port (#447) (#453) 4f7f7abe PORT set in a project's .env was ignored: the CLI computed the port from process.env.PORT || 8080 in bin/webjs.js BEFORE the server's bootstrap ran process.loadEnvFile('.env'), so the file's PORT never reached the comparison and the server always bound 8080. Every other .env var worked because the
server
v0.8.21
6 changes
Features
webjs check flags server-only imports that reach the browser (#457) 471531fc * feat: webjs check flags server-only imports that reach the browser
A page/layout/component that ships to the browser but transitively imports a server-only .server.{ts,js} module crashes at runtime (the
convention-pick redirect() status + hint bare webjs dev's missing prisma client (#456) 5d5cd76b * feat: convention-pick redirect() status (302 GET gate, 307 action)
redirect('/login') baked in 307 unconditionally, so a GET-to-GET gate (an auth bounce) returned the method-preserving 307 where 302 Found is
type the auth() session user (augmentable + generic) (#454) 5298fbe4 * feat: type the auth() session user (augmentable + generic)
auth() resolved { user: Record<string, unknown> }, so reading a custom field the session/jwt callbacks set (e.g. session.user.id, which crisp
make the opt-in webjs vendor pin output committable (#455) 3e125a1f * feat: make committable the opt-in webjs vendor pin output
Vendoring is an optional opt-in: the no-build default resolves bare specifiers at runtime via jspm.io and commits nothing. But when a user
flag an incoherent importmap dependency graph in webjs doctor (#460) 06c10efa * feat(server): importmap coherence check over the resolved dep graph
Add checkImportmapCoherence: given a produced importmap, for each resolved package it verifies that the version pinned for every OTHER resolved package it
Fixes
resolve the whole bare-import set in one jspm generate call (#459) 105cb8d7 * fix: resolve the whole bare-import set in one jspm generate call
Per-package-in-isolation resolution pinned a directly-imported package to its local node_modules version while a transitive floated to
core
v0.7.15
1 change
Features
convention-pick redirect() status + hint bare webjs dev's missing prisma client (#456) 5d5cd76b * feat: convention-pick redirect() status (302 GET gate, 307 action)
redirect('/login') baked in 307 unconditionally, so a GET-to-GET gate (an auth bounce) returned the method-preserving 307 where 302 Found is
intellisense
v0.5.1
1 change
Features
flag duplicate custom-element tag registrations (check rule + editor 9004) (#444) 5ff76b63 * feat: add no-duplicate-tag webjs check rule (#364)
* feat: flag duplicate custom-element tag registrations in editor (#364)
server
v0.8.20
1 change
Features
flag duplicate custom-element tag registrations (check rule + editor 9004) (#444) 5ff76b63 * feat: add no-duplicate-tag webjs check rule (#364)
* feat: flag duplicate custom-element tag registrations in editor (#364)
core
v0.7.14
1 change
Fixes
type WebComponent base lifecycle callbacks as non-optional (#433) (#434) e73895c7 component.d.ts declared connectedCallback / disconnectedCallback / attributeChangedCallback as OPTIONAL, but component.js concretely implements all three. So an override calling super.connectedCallback() (the documented, required pattern, used by the scaffold's own theme-toggle.ts) errored with
core
v0.7.13
1 change
Fixes
declare drifted @webjsdev/core exports in index.d.ts + add a coverage guard (#428) 0cd345fc * fix: declare the 35 drifted @webjsdev/core exports in index.d.ts (#388)
The hand-maintained index.d.ts overlay had drifted from index.js: 35 runtime named exports (signal/computed/effect/batch/isSignal/Signal, the full directive
Phase 3 (#386) removes the ts-lit-plugin dependency, so the tests that simulated ts-lit diagnostics and asserted the suppression path no longer
self-contained editor plugins (bundle ts-plugin in nvim too) + drop ui pin (#401) 42db65ef * feat: bundle @webjsdev/ts-plugin into webjs.nvim (self-contained)
#398. webjs.nvim now bundles the language-service plugin, so a Neovim user gets intelligence even when the app has no @webjsdev/ts-plugin in
extract the MCP server into a standalone @webjsdev/mcp package (#417) b4c86b67 * feat: extract the MCP server into a standalone @webjsdev/mcp package (#415)
The MCP server lived inside @webjsdev/cli (lib/mcp.js + mcp-docs.js + mcp-source.js + check-json.js), reachable ONLY as webjs mcp. Following
Fixes
scaffold @types/node so node: builtins resolve in apps (#392) 7ddd36e9
core
v0.7.12
2 changes
Fixes
type notFound/redirect as never so they narrow as terminators (#390) 2fc9c007 * Type nav sentinels as never
* test: add type fixture proving notFound/redirect narrow as never (#390)
add types conditions + declarations for @webjsdev/core subpaths (#389) a44ceb46 * Add types conditions for core subpaths
Phase 4 of the editor-plugin epic (#381), the Neovim counterpart to the webjs VS Code extension. No Lit dependency.
self-contained editor plugins (bundle ts-plugin in nvim too) + drop ui pin (#401) 42db65ef * feat: bundle @webjsdev/ts-plugin into webjs.nvim (self-contained)
#398. webjs.nvim now bundles the language-service plugin, so a Neovim user gets intelligence even when the app has no @webjsdev/ts-plugin in
nvim
v0.2.0
1 change
Features
add a vtsls/LazyVim LSP helper (#430) aa78b1d8with_tsserver_plugin() emits the ts_lsinit_options.plugins shape, which does nothing under vtsls (LazyVim's default TypeScript LSP). Adds vtsls_global_plugin() + with_vtsls_plugin(settings) (idempotent, pointing at the bundled @webjsdev/intellisense), so a LazyVim user wires intelligence with a one-line helper instead of hand-writing globalPlugins. README gets a LazyVim recipe alongside the ts_ls one; the vimdoc + selftest cover both shapes (#405).
mcp
v0.1.0
1 change
Features
the webjs MCP server, now a standalone package (#415) The read-only Model Context Protocol server that gives AI coding agents the live introspection surface (routes / actions / components / check) plus the framework knowledge layer (docs, recipes, source) moved out of @webjsdev/cli into its own package. Register it with any MCP host via npx @webjsdev/mcp; webjs mcp keeps working by delegating to it. Following the next-devtools-mcp model.
intellisense
v0.5.0
1 change
Features
renamed from @webjsdev/ts-plugin to @webjsdev/intellisense (#416) The standalone editor-intelligence package (in-template go-to-definition, binding-aware completions, value/binding diagnostics, and hover inside ` html… templates) is now named for what it does rather than how it is implemented. Same code, same standalone design (its own template parser, no Lit dependency). The tsconfig.jsonplugins entry, the VS Code extension bundle, and the webjs.nvim vendor are all updated to @webjsdev/intellisense. The prior @webjsdev/ts-plugin` releases stay recorded under their own changelog. webjs has no published users yet, so this is a clean rename with no compatibility shim.
server
v0.8.19
1 change
Fixes
hoist head tags past interleaved HTML comments (#406) (#407) bc105f05 A contiguous leading run of <script>/<style>/<link> tags is hoisted out of the rendered body into <head> so render-blocking assets land where the browser honours them. The scanner stopped at the first non-tag token, so an HTML comment interleaved with the run (e.g. "<!-- Self-hosted fonts -->"
ts-plugin
v0.5.0
1 change
Features
editor plugin Phase 2: in-template language service (ts-lit-plugin parity) (#391) 3113960c * feat: add webjs in-template HTML parser (Phase 2 foundation)
Phase 2 of the editor-plugin epic (#381, #385) rebuilds ts-lit-plugin's in-template intelligence inside @webjsdev/ts-plugin so webjs can drop the
vscode
v0.2.0
2 changes
Features
add all-in-one webjs VSCode extension (no Lit plugin) (#384) baa82bab * feat: add all-in-one webjs VSCode extension (no Lit plugin)
Phase 1 of the editor-plugin epic (#381). Ships a self-contained VSCode extension that gives webjs apps html/css/svg template
editor plugin Phase 2: in-template language service (ts-lit-plugin parity) (#391) 3113960c * feat: add webjs in-template HTML parser (Phase 2 foundation)
Phase 2 of the editor-plugin epic (#381, #385) rebuilds ts-lit-plugin's in-template intelligence inside @webjsdev/ts-plugin so webjs can drop the
cli
v0.10.12
2 changes
Features
add a knowledge + authoring layer to webjs mcp (Next-MCP parity) (#377) 83abc2c4 * feat: add a knowledge + authoring layer to webjs mcp (#376)
webjs mcp shipped four read-only introspection tools (the Next.js /_next/mcp equivalent). This adds the second layer the next-devtools-mcp package showed is
add a source tool to webjs mcp to read the no-build framework source (#379) ea4ddd27 * feat: add a source tool to webjs mcp to read the no-build framework source (#378)
webjs is no-build, so node_modules/@webjsdev/*/src is the real JSDoc that runs. Surface that through the MCP:
cli
v0.10.11
2 changes
Features
enforce scaffold-content removal via a check rule + markers (#359) (#360) c631ffe3 * feat: enforce scaffold-content removal via a check rule + markers (#359)
* docs: document the no-scaffold-placeholder enforcement (#359)
Fixes
ignore .webjs/routes.d.ts at any depth via /.webjs/* (#366) 2207bf3e * fix: ignore .webjs/routes.d.ts at any depth via /.webjs/*
The gitignore pattern .webjs/* carries a slash, so git anchors it to the .gitignore's own directory. In this monorepo the nested in-repo apps
server
v0.8.18
3 changes
Features
enforce scaffold-content removal via a check rule + markers (#359) (#360) c631ffe3 * feat: enforce scaffold-content removal via a check rule + markers (#359)
* docs: document the no-scaffold-placeholder enforcement (#359)
Fixes
ignore .webjs/routes.d.ts at any depth via /.webjs/* (#366) 2207bf3e * fix: ignore .webjs/routes.d.ts at any depth via /.webjs/*
The gitignore pattern .webjs/* carries a slash, so git anchors it to the .gitignore's own directory. In this monorepo the nested in-repo apps
version nested relative import specifiers to match modulepreload (#369) (#370) 0e547046 * fix: version nested relative import specifiers to match modulepreload (#369)
A layout/page imports its components with a bare relative specifier (import '../components/x.ts'). The browser resolves that against the
ui
v0.3.5
1 change
Features
support closest() and host IDL reflections in the SSR shim (#228) a9c26ea7 * feat: support closest() and host IDL reflections in the SSR shim
Compound components derive active/pressed state by walking to a parent with this.closest('parent-tag') and reading its state. The server
cli
v0.10.10
7 changes
Features
drive a webjs-frame by id from outside it, plus _top and aria-busy (#338) 430e6cca * feat: drive a webjs-frame by id from outside it, plus aria-busy
A swap was scoped to a frame only when the trigger was DOM-nested inside it (closest-only). Add external frame targeting: a link or form carrying
recover from a failed navigation in place instead of a full reload (#339) 43eaf64f * feat: recover from a failed navigation in place instead of a full reload
On a non-HTML error response (a JSON 500), a transport failure, or an unparseable HTML body, fetchAndApply did a destructive location.href full
view transitions on partial swaps + data-webjs-permanent (#343) 77c1216d View Transitions only wrapped the full-body fallback, never the partial layout-marker swap or the webjs-frame swap (the common designed-for paths). Wrap all three swap paths in document.startViewTransition via runWithTransition, gated by an opt-in <meta name="view-transition" content="same-origin"> (OFF
src-driven <webjs-frame> self-loading + server subtree render (#346) 86f6ae44 * feat: src-driven webjs-frame self-loading + server subtree render
A <webjs-frame> could not self-load a region's content, forcing the fetch-in-handler anti-pattern for deferred content (comments, a recommendations
add the <webjs-stream> stream-action protocol (HTTP + live channel) (#347) a489c97d * feat: add the <webjs-stream> surgical DOM-update applier
Ship one small custom element that reads a server-sent <template> carrying an action + target id and applies it via native DOM (append/prepend/before/after/
provide an opt-in progressive-enhancement service worker primitive (#350) ced742f8 * feat: ship an opt-in progressive-enhancement service worker primitive
Scaffold a hand-authored public/sw.js + public/offline.html into every app (copied by create.js, dormant until the app registers it, so the JS-disabled
Fixes
soften the scaffolded test-with-source gate to a warning (#331) a6291b34 The scaffolded require-tests-with-src.sh hard-blocked (exit 2) a commit that staged app code with no test, contradicting the convention-vs-check principle the same scaffolded AGENTS.md / CONVENTIONS.md state (a sensible app can legitimately want a test-less commit, so every change ships with a test is a
server
v0.8.17
5 changes
Features
fold an app-source fingerprint into the HTML cache key (#329) c4dbb8a9 * feat: fold an app-source fingerprint into the HTML cache key
The #241 HTML cache key folds the published build id, but that is a fingerprint of the IMPORTMAP (core + vendor) only, so a deploy that changes ONLY an app
form submission-state events + aria-busy + an optimistic() helper (#340) 342ae484 A form submitting through the JS-enhanced router had no signal for a pending state. The router now sets the native aria-busy on the form for the in-flight duration (the readable is-this-form-submitting primitive) and dispatches a bubbling webjs:submit-start ({ form, url }) at the start and webjs:submit-end
src-driven <webjs-frame> self-loading + server subtree render (#346) 86f6ae44 * feat: src-driven webjs-frame self-loading + server subtree render
A <webjs-frame> could not self-load a region's content, forcing the fetch-in-handler anti-pattern for deferred content (comments, a recommendations
add the <webjs-stream> stream-action protocol (HTTP + live channel) (#347) a489c97d * feat: add the <webjs-stream> surgical DOM-update applier
Ship one small custom element that reads a server-sent <template> carrying an action + target id and applies it via native DOM (append/prepend/before/after/
ship a rich dev error overlay pushed live over SSE (#348) 30772213 * feat: ship a rich dev error overlay pushed live over SSE
A dev SSR render crash, a non-erasable-TS strip failure, and a failed rebuild each push a structured error frame (message, parsed file:line:col, a source
core
v0.7.11
8 changes
Features
drive a webjs-frame by id from outside it, plus _top and aria-busy (#338) 430e6cca * feat: drive a webjs-frame by id from outside it, plus aria-busy
A swap was scoped to a frame only when the trigger was DOM-nested inside it (closest-only). Add external frame targeting: a link or form carrying
recover from a failed navigation in place instead of a full reload (#339) 43eaf64f * feat: recover from a failed navigation in place instead of a full reload
On a non-HTML error response (a JSON 500), a transport failure, or an unparseable HTML body, fetchAndApply did a destructive location.href full
form submission-state events + aria-busy + an optimistic() helper (#340) 342ae484 A form submitting through the JS-enhanced router had no signal for a pending state. The router now sets the native aria-busy on the form for the in-flight duration (the readable is-this-form-submitting primitive) and dispatches a bubbling webjs:submit-start ({ form, url }) at the start and webjs:submit-end
view transitions on partial swaps + data-webjs-permanent (#343) 77c1216d View Transitions only wrapped the full-body fallback, never the partial layout-marker swap or the webjs-frame swap (the common designed-for paths). Wrap all three swap paths in document.startViewTransition via runWithTransition, gated by an opt-in <meta name="view-transition" content="same-origin"> (OFF
src-driven <webjs-frame> self-loading + server subtree render (#346) 86f6ae44 * feat: src-driven webjs-frame self-loading + server subtree render
A <webjs-frame> could not self-load a region's content, forcing the fetch-in-handler anti-pattern for deferred content (comments, a recommendations
add the <webjs-stream> stream-action protocol (HTTP + live channel) (#347) a489c97d * feat: add the <webjs-stream> surgical DOM-update applier
Ship one small custom element that reads a server-sent <template> carrying an action + target id and applies it via native DOM (append/prepend/before/after/
ship a rich dev error overlay pushed live over SSE (#348) 30772213 * feat: ship a rich dev error overlay pushed live over SSE
A dev SSR render crash, a non-erasable-TS strip failure, and a failed rebuild each push a structured error frame (message, parsed file:line:col, a source
Fixes
reconcile plain .map() arrays in place instead of rebuilding (#353) (#354) 950026a8 * fix: reconcile plain .map() arrays in place instead of rebuilding
A .map() / list interpolation used to rebuild its entire child part on ANY change to the array's rendered output, replacing every DOM node. The
cli
v0.10.9
1 change
Features
add a read-only webjs MCP server and webjs check --json (#327) 42f24a9e * feat: add a read-only webjs MCP server and webjs check --json
Two read-only surfaces over data webjs already computes. (1) webjs check --json emits the structured Violation[] checkConventions already returns plus a summary
server
v0.8.16
2 changes
Features
ship a file-storage primitive for uploaded File/Blob payloads (#326) ccc9cff2 * feat: ship a file-storage primitive for uploaded File/Blob payloads
webjs round-trips File/Blob/FormData over the wire but had no answer for where the bytes land. Add a minimal pluggable FileStore with a streaming local-disk
add a read-only webjs MCP server and webjs check --json (#327) 42f24a9e * feat: add a read-only webjs MCP server and webjs check --json
Two read-only surfaces over data webjs already computes. (1) webjs check --json emits the structured Violation[] checkConventions already returns plus a summary
server
v0.8.15
1 change
Features
ship type declarations for @webjsdev/server (fix TS7016) (#322) 50428c47
@webjsdev/server now ships a hand-authored index.d.ts overlay and types export conditions, so a TypeScript app under strict + nodenext resolves real types for import { createRequestHandler, cors, cache, ... } from '@webjsdev/server' instead of emitting TS7016. The runtime stays plain .js + JSDoc; the overlay is types-only with zero runtime cost. The ./check and ./testing subpaths are typed too.
cli
v0.10.8
2 changes
Features
add a hydrating ssrFixture() and an opt-in a11y assertion (#317) 26ea8841 * feat: add a hydrating ssrFixture() and an opt-in a11y assertion to the test layer
The shipped fixture() renders via renderToString and parses HTML into a container but never drives client hydration or awaits a true update cycle (waitForUpdate yielded two macrotasks, never el.updateComplete), and nothing did accessibility testing.
Fixes
strike the phantom check --fix flag and anchor runtime errors to docs (#316) ec189feb * fix: strike the phantom check --fix flag and anchor two runtime errors to docs
AGENTS.md and packages/cli/AGENTS.md advertised webjs check --fix, but bin/webjs.js only handles --rules; --fix was never read, so an agent running it saw violations printed unchanged and assumed its code was fixed (false confidence). None of the rules can be auto-fixed safely (they rewrite code or rename files breaking imports), so strike --fix from the docs rather than ship a risky codemod, and note in the cli docs that check is report-only with a prose fix hint per violation.
App modules and public assets shipped public, max-age=3600 because their URLs were un-versioned, so immutable caching was unsafe (a deploy that
Fixes
strike the phantom check --fix flag and anchor runtime errors to docs (#316) ec189feb * fix: strike the phantom check --fix flag and anchor two runtime errors to docs
AGENTS.md and packages/cli/AGENTS.md advertised webjs check --fix, but bin/webjs.js only handles --rules; --fix was never read, so an agent running it saw violations printed unchanged and assumed its code was fixed (false confidence). None of the rules can be auto-fixed safely (they rewrite code or rename files breaking imports), so strike --fix from the docs rather than ship a risky codemod, and note in the cli docs that check is report-only with a prose fix hint per violation.
core
v0.7.10
2 changes
Features
add a hydrating ssrFixture() and an opt-in a11y assertion (#317) 26ea8841 * feat: add a hydrating ssrFixture() and an opt-in a11y assertion to the test layer
The shipped fixture() renders via renderToString and parses HTML into a container but never drives client hydration or awaits a true update cycle (waitForUpdate yielded two macrotasks, never el.updateComplete), and nothing did accessibility testing.
App modules and public assets shipped public, max-age=3600 because their URLs were un-versioned, so immutable caching was unsafe (a deploy that
cli
v0.10.7
3 changes
Features
add a webjs typecheck command (tsc --noEmit wrapper) (#307) 0eb4e9f4 webjs ships erasable TypeScript as a first-class authoring mode (strict + noEmit + erasableSyntaxOnly + @webjsdev/ts-plugin) but had no command to run the type checker. The runtime only catches strip-time failures (non-erasable syntax) with a 500; genuine type errors were surfaced by nothing, since webjs check is correctness-only and explicitly does not type-check.
Add webjs typecheck: it resolves the project's OWN typescript/bin/tsc (via createRequire from the app cwd, so it reads the app's tsconfig) and spawns it with --noEmit, passing extra args through, exiting non-zero on a type error so it works as a CI gate. When TypeScript is not installed it prints a clear message and exits non-zero. The framework runs the standard compiler, it does not embed one. The scaffold now ships a typecheck npm script and typescript as a devDependency, with CONVENTIONS.md documenting it as the separate is-my-TypeScript-valid gate to add to CI once the app type-checks cleanly. Not auto-added to the scaffold CI to avoid a day-one failure before the app is verified to type-check.
document the handle() test harness, ship test helpers, fix the saas auth test (#308) 9df85d23 createRequestHandler().handle(request) drives the full pipeline (middleware, routing, SSR, actions, auth/CSRF) and the framework's own suite uses it, but it was documented only as an embedding API, with no testRequest() recipe, no helper to build an authenticated/CSRF request, and no helper to round-trip an action through the /__webjs/action/<hash>/<fn> serializer+dispatch path. Actions were tested by direct import, bypassing CSRF and prod error sanitization. The saas template's test asserted only TypeScript shapes and sat on test/unit/, contradicting the documented test/<feature>/ convention.
Add packages/server/src/testing.js (a ./testing export): testRequest fires a native Request through handle(); getCsrf mints a valid token+cookie off the first SSR response (reusing the real csrf.js constants); loginAndGetCookies drives the real credentials login and captures the genuine signed session cookie; actionEndpoint computes the real /__webjs/action/<hash>/<fn> path; and invokeActionForTest round-trips an action through that endpoint with the real serializer + CSRF, so a regression test proves it catches what a direct import misses (a Map surviving only because the wire serializer ran, a 403 on a CSRF-missing request, and prod error sanitization hiding a stack + secret field the direct throw leaks). The saas scaffold now ships a real auth-flow test at test/auth/auth.test.ts: the unauthenticated-protected-route-redirect always runs, and the signup/login/authenticated-render path runs when the DB is set up (skips cleanly otherwise).
add a webjs doctor project-health command (#311) 44ff8600 * feat: add a webjs doctor project-health command
There was no single command to verify a webjs project is set up correctly. webjs has unusually many fragile preconditions (the Node 24+ strip-types floor, the erasableSyntaxOnly TS flag, importmap pin freshness, git-hook activation, env drift, @webjsdev version coherence), each an independent failure mode a contributor onboarding to an existing repo hits only at runtime.
server
v0.8.13
5 changes
Features
emit SRI integrity for live-resolved vendor imports (#300) 72ae06f9 * feat: emit SRI integrity for live-resolved (unpinned) vendor imports
SRI was already computed when an app pins (the pin importmap carries sha384 integrity), but an app with no pin file (the in-repo norm and a fresh scaffold) hit the live-resolve path, which returned integrity:{}, so cross-origin jspm.io modules served with crossorigin but no integrity attribute. A swapped or compromised CDN response then executed unverified.
add sitemap() and sitemapIndex() helpers for spec-valid XML (#306) 9cc6d9af * feat: add sitemap() and sitemapIndex() helpers for spec-valid XML
An author of app/sitemap.{js,ts} had to hand-construct the entire <urlset> XML string, escaping URLs and formatting lastmod dates by hand, and a malformed string is silently rejected by search engines. There was also no sitemap-index support, so a site over the 50,000-URL / 50MB per-file limit had no built-in sharding path.
document the handle() test harness, ship test helpers, fix the saas auth test (#308) 9df85d23 createRequestHandler().handle(request) drives the full pipeline (middleware, routing, SSR, actions, auth/CSRF) and the framework's own suite uses it, but it was documented only as an embedding API, with no testRequest() recipe, no helper to build an authenticated/CSRF request, and no helper to round-trip an action through the /__webjs/action/<hash>/<fn> serializer+dispatch path. Actions were tested by direct import, bypassing CSRF and prod error sanitization. The saas template's test asserted only TypeScript shapes and sat on test/unit/, contradicting the documented test/<feature>/ convention.
Add packages/server/src/testing.js (a ./testing export): testRequest fires a native Request through handle(); getCsrf mints a valid token+cookie off the first SSR response (reusing the real csrf.js constants); loginAndGetCookies drives the real credentials login and captures the genuine signed session cookie; actionEndpoint computes the real /__webjs/action/<hash>/<fn> path; and invokeActionForTest round-trips an action through that endpoint with the real serializer + CSRF, so a regression test proves it catches what a direct import misses (a Map surviving only because the wire serializer ran, a 403 on a CSRF-missing request, and prod error sanitization hiding a stack + secret field the direct throw leaks). The saas scaffold now ships a real auth-flow test at test/auth/auth.test.ts: the unauthenticated-protected-route-redirect always runs, and the signup/login/authenticated-render path runs when the DB is set up (skips cleanly otherwise).
input validation on the RPC action path, shared with expose() (#309) 4cbe55d9 * feat: input validation on the RPC action path, shared with expose()
The primary way actions are called (importing them from a client component, which becomes the RPC stub) had no input-validation seam: invokeAction deserialized args and called fn(...args) with no hook. A validate option was honored only on the expose() REST path, so a validator could not be declared once and shared by both paths, and a REST validation failure was a thrown 400 rather than a structured field-error result an app can render.
add a webjs doctor project-health command (#311) 44ff8600 * feat: add a webjs doctor project-health command
There was no single command to verify a webjs project is set up correctly. webjs has unusually many fragile preconditions (the Node 24+ strip-types floor, the erasableSyntaxOnly TS flag, importmap pin freshness, git-hook activation, env drift, @webjsdev version coherence), each an independent failure mode a contributor onboarding to an existing repo hits only at runtime.
core
v0.7.9
1 change
Features
input validation on the RPC action path, shared with expose() (#309) 4cbe55d9 * feat: input validation on the RPC action path, shared with expose()
The primary way actions are called (importing them from a client component, which becomes the RPC stub) had no input-validation seam: invokeAction deserialized args and called fn(...args) with no hook. A validate option was honored only on the expose() REST path, so a validator could not be declared once and shared by both paths, and a REST validation failure was a thrown 400 rather than a structured field-error result an app can render.
cli
v0.10.6
3 changes
Features
generate typed route props and a route union for navigate() (#293) c18cc625 * feat: generate typed route props and a route union for navigate()
A page/layout/route-handler default export receives { params, searchParams, url }, but webjs gave it no type, so searchParams was untyped everywhere and a [slug] route's params had to be hand-annotated with nothing tying the key to the folder name. Renaming [slug] to [id] silently broke every params.slug reference, and navigate('/blgo/123') type-checked fine only to 404 at runtime.
type the package.json webjs.* config block (#295) 19dde265 * feat: type the package.json webjs.* config block
webjs reads a webjs.* object from package.json (elide, headers, redirects, csp, trailingSlash, and the body-limit / timeout knobs), but there was no type, schema, or validation, so a typo'd key was silently dropped and the feature it toggled stayed at default with no diagnostic. The config equivalent of an untyped API that fails open.
Fixes
surface a missing webjs-frame instead of a silent full-page swap (#294) fdbfc999 * fix: surface a missing webjs-frame instead of a silent full-page swap
A frame-scoped navigation whose response omits the requested <webjs-frame id> silently blew away the whole page. In applySwap the frame branch was gated on (target && source); when the response lacked a matching frame (source null) or the target frame was gone (target null), control fell through to the layout-marker swap and then the full-body replaceChildren, wholesale-replacing the document with no warning and no event. An auth redirect returning a login page without the frame thus destroyed the page.
server
v0.8.12
6 changes
Features
add a declarative webjs.redirects config for SEO redirects (#290) f2dcf03f * feat: add a declarative webjs.redirects config for SEO redirects
A package.json webjs.redirects array of { source, destination, permanent?, statusCode? } applies before routing: a URLPattern source
add a webjs.trailingSlash policy that canonicalizes with a 308 (#291) c9d7d9af * feat: add a webjs.trailingSlash policy that canonicalizes with a 308
A page reachable at both /about and /about/ is duplicate content. webjs.trailingSlash ('never' strips, 'always' adds, 'ignore' default
generate typed route props and a route union for navigate() (#293) c18cc625 * feat: generate typed route props and a route union for navigate()
A page/layout/route-handler default export receives { params, searchParams, url }, but webjs gave it no type, so searchParams was untyped everywhere and a [slug] route's params had to be hand-annotated with nothing tying the key to the folder name. Renaming [slug] to [id] silently broke every params.slug reference, and navigate('/blgo/123') type-checked fine only to 404 at runtime.
type the package.json webjs.* config block (#295) 19dde265 * feat: type the package.json webjs.* config block
webjs reads a webjs.* object from package.json (elide, headers, redirects, csp, trailingSlash, and the body-limit / timeout knobs), but there was no type, schema, or validation, so a typo'd key was silently dropped and the feature it toggled stayed at default with no diagnostic. The config equivalent of an untyped API that fails open.
add JSON-LD structured data to the metadata API (#296) 0554d1c2 * feat: add JSON-LD structured data to the metadata API
webjs had a near-complete metadata API but no way to emit JSON-LD, the highest-leverage modern SEO surface (Article, Product, BreadcrumbList, Organization, FAQ rich results) that Google reads only from <script type="application/ld+json">. Authors had to hand-inject a raw script via unsafeHTML, which is unergonomic and easy to get wrong.
support webjs.basePath for sub-path deployments (#298) aafee8f5 * feat: support webjs.basePath for sub-path deployments
An app deployed under a sub-path (example.com/app/) behind a proxy that does not strip the prefix was broken: every framework-emitted absolute URL (the importmap targets, modulepreload hints, the boot script's /__webjs/core/* specifiers and per-route module URLs, the dev reload src) assumed the app sat at the origin root, so they pointed at /__webjs/core/* instead of /app/__webjs/core/*, module resolution 404d, and the page never hydrated. createRequestHandler explicitly targets embedding, where a sub-path mount is the norm.
core
v0.7.8
8 changes
Features
re-render page server actions with field errors and preserved input (#277) bcd11a85 * feat: re-render page server actions with field errors and preserved input
A page.{js,ts} may export action({ request, params, searchParams, url, formData }). A non-GET form posting to the page runs it: a success
mint a per-request CSP nonce and emit the Content-Security-Policy header (#279) 79958c45 * feat: mint a per-request CSP nonce and emit the Content-Security-Policy header
webjs's CSP support was consume-only (ssr.js read a nonce from the inbound request header). Now, when webjs.csp is enabled, the handler
export a Metadata type for metadata and generateMetadata (#292) d63b2ae9 * feat: export a Metadata type for metadata and generateMetadata
A page exports metadata / generateMetadata returning a metadata object, but webjs typed nothing, so a typo (titel, descripton, a wrong openGraph
generate typed route props and a route union for navigate() (#293) c18cc625 * feat: generate typed route props and a route union for navigate()
A page/layout/route-handler default export receives { params, searchParams, url }, but webjs gave it no type, so searchParams was untyped everywhere and a [slug] route's params had to be hand-annotated with nothing tying the key to the folder name. Renaming [slug] to [id] silently broke every params.slug reference, and navigate('/blgo/123') type-checked fine only to 404 at runtime.
type the package.json webjs.* config block (#295) 19dde265 * feat: type the package.json webjs.* config block
webjs reads a webjs.* object from package.json (elide, headers, redirects, csp, trailingSlash, and the body-limit / timeout knobs), but there was no type, schema, or validation, so a typo'd key was silently dropped and the feature it toggled stayed at default with no diagnostic. The config equivalent of an untyped API that fails open.
add JSON-LD structured data to the metadata API (#296) 0554d1c2 * feat: add JSON-LD structured data to the metadata API
webjs had a near-complete metadata API but no way to emit JSON-LD, the highest-leverage modern SEO surface (Article, Product, BreadcrumbList, Organization, FAQ rich results) that Google reads only from <script type="application/ld+json">. Authors had to hand-inject a raw script via unsafeHTML, which is unergonomic and easy to get wrong.
support webjs.basePath for sub-path deployments (#298) aafee8f5 * feat: support webjs.basePath for sub-path deployments
An app deployed under a sub-path (example.com/app/) behind a proxy that does not strip the prefix was broken: every framework-emitted absolute URL (the importmap targets, modulepreload hints, the boot script's /__webjs/core/* specifiers and per-route module URLs, the dev reload src) assumed the app sat at the origin root, so they pointed at /__webjs/core/* instead of /app/__webjs/core/*, module resolution 404d, and the page never hydrated. createRequestHandler explicitly targets embedding, where a sub-path mount is the norm.
Fixes
surface a missing webjs-frame instead of a silent full-page swap (#294) fdbfc999 * fix: surface a missing webjs-frame instead of a silent full-page swap
A frame-scoped navigation whose response omits the requested <webjs-frame id> silently blew away the whole page. In applySwap the frame branch was gated on (target && source); when the response lacked a matching frame (source null) or the target frame was gone (target null), control fell through to the layout-marker swap and then the full-body replaceChildren, wholesale-replacing the document with no warning and no event. An auth redirect returning a login page without the frame thus destroyed the page.
server
v0.8.11
4 changes
Features
add access logging, request id, an error hook, and a version probe (#285) b3540a7d
Each handled request emits one structured access log line (method, path, status, durationMs, requestId) via the pluggable logger. A per-request id is minted with native crypto (or an inbound X-Request-Id honored after a charset/length check), stored on the request ALS, set on X-Request-Id, and exposed via requestId(). createRequestHandler({ onError }) is a best-effort APM sink called on a 500 / middleware / ssr / action throw with the original error + request id. GET /__webjs/version returns { version, build, node, uptime } so a deploy can verify the live build.
honor If-None-Match with a 304 on cacheable pages, assets, and modules (#286) 7df66c67
A shared RFC 7232 helper at the response funnel sets a weak content-hash ETag on cacheable buffered GET/HEAD responses (a page with a public metadata.cacheControl, static assets, app + runtime modules) and returns 304 Not Modified when If-None-Match matches, so a repeat request no longer re-transfers an identical body. no-store / private pages and streamed responses are excluded; an internal buffered marker gates which responses are hashed so a streaming route or SSE never hangs.
cache static-route HTML with a revalidate window and revalidatePath (#287) 3aabec25
A page opts into server-side HTML caching with export const revalidate = N (the no-build equivalent of ISR): a GET within the window serves the cached body from the pluggable store without re-running the page function, with the CSRF cookie re-minted per response. It is safe by construction: a page that reads per-user state via cookies() / headers() / getSession() / auth() is auto-excluded (Next's auto-dynamic model), and CSP / non-framework-Set-Cookie / partial-nav responses are never cached. revalidatePath(path) / revalidateAll() evict on demand.
add tag-based and action-driven invalidation for server cache() (#288) fd029d5e
cache(fn, { key, ttl, tags }) records a thin tag-to-key index in the store; tags can be a static array or a function (...args) => string[] so a per-arg read tags with the entity id. revalidateTag(tag) / revalidateTags(tags) evict every cached key under a tag across modules, so a mutation in one module invalidates a related read in another without importing its wrapper. Pairs with revalidatePath as the server cache invalidation surface.
cli
v0.10.5
3 changes
Features
fail fast on Node below 24 with a clear preflight (#282) a44583e1
The CLI runs a dependency-free Node-version preflight at the very top (before importing @webjsdev/server), so a Node below 24 exits with a clear, actionable message naming the found and required version instead of a cryptic link-time error. The new lib/node-preflight.js reads the required major from the package's own engines.node.
demonstrate cors() in the api scaffold (#280) 6527ac81
webjs create --template api now scaffolds a root middleware.ts applying the new cors() primitive with an explicit origin allowlist, so a fresh API app ships correct, safe CORS rather than a hand-rolled one.
scaffold the saas signup form as a no-JS page action (#277) bcd11a85
The saas template's signup page now posts to a page action that validates and re-renders with field errors and preserved input (the canonical progressive-enhancement form pattern), replacing the prior inert form that silently lost data on submit.
server
v0.8.10
7 changes
Features
re-render page server actions with field errors and preserved input (#277) bcd11a85
A page.{js,ts} may export action({ request, params, searchParams, url, formData }). A non-GET form posting to the page runs it: a success result (or a thrown redirect()) responds 303 (Post/Redirect/Get), and a failure result re-renders the same page (422) with the result on ctx.actionData so the page repopulates field errors and the submitted values. Works with JS disabled (native form POST) and enhanced (the client router applies the 422 in place and follows the 303). The ActionResult envelope gains additive fieldErrors / values / redirect.
emit secure-by-default response headers with a per-path config (#278) dbd3e619
Every served response now carries X-Content-Type-Options, X-Frame-Options, Referrer-Policy, and Permissions-Policy, plus Strict-Transport-Security in production over HTTPS only (forwarded-proto aware). A package.jsonwebjs.headers array (a URLPattern source plus key/value pairs) adds, overrides, or disables (value: null) a header per path. Precedence is secure defaults < path config < app middleware (a header the app already set is never clobbered).
mint a per-request CSP nonce and emit the Content-Security-Policy header (#279) 79958c45
CSP was consume-only (the SSR pipeline read a nonce from the inbound request header). When webjs.csp is enabled, the handler now mints a fresh CSPRNG nonce per request, stamps the same value on every inline script / importmap / modulepreload, and sets a matching Content-Security-Policy response header through the headers seam. Off by default; true enables a strict-dynamic + nonce policy, or an object customizes directives / report-only.
add a cors() middleware primitive for route handlers (#280) 6527ac81
cors({ origin, credentials, methods, allowedHeaders, exposedHeaders, maxAge }) is exported from @webjsdev/server as a webjs middleware usable in middleware.js or around a route.js handler. origin accepts a string, array (strings and RegExps), RegExp, predicate, or '*'. An OPTIONS preflight short-circuits a 204; an actual request reflects an allowed origin and appends Vary: Origin. credentials never emits a wildcard ACAO (and warns once on a wildcard origin). The shared resolver also backs the expose() path.
cap request body size (413) and set server timeouts (#281) 7fe3b21e
Every request body read (RPC actions, exposed REST, route.jsreadBody, page-action forms, credentials sign-in) goes through one helper that fast-rejects an over-limit Content-Length and cancels a chunked stream the moment it crosses the cap, so an oversized body never buffers into memory; over the limit responds 413. Defaults 1 MiB JSON/RPC and 10 MiB multipart, configurable. The node:http server gets requestTimeout / headersTimeout / keepAliveTimeout to close the slowloris exposure.
fail fast on Node below 24 with a clear preflight (#282) a44583e1
createRequestHandler now throws a clear, actionable error at boot on a Node below 24 (webjs relies on Node 24's built-in TypeScript strip and recursive fs.watch). dev.js namespace-imports node:module so the package links on old Node and the message wins instead of a cryptic link-time SyntaxError. The minimum is read from the package's own engines.node.
validate env vars at boot via an optional env schema (#283) d958bec2
An optional app-root env.{js,ts} default-exports a schema object (NAME to a type or { type, required, optional, default, values, minLength, pattern }; types string/number/boolean/url/enum) or a function (env) escape hatch. createRequestHandler runs it after the .env load, collects every error at once, coerces and applies defaults back into process.env, and fails fast with a message naming each offending var (never echoing the value). Absent env.{js,ts} is a no-op.
core
v0.7.7
2 changes
Features
support closest() and host IDL reflections in the SSR shim (#228) a9c26ea7
The server element shim now implements closest() (tag-name selectors, resolved against an ancestor chain the SSR walker threads through each instance) plus the host IDL reflections a render() mutates on this (dataset, className, hidden, id/title/slot/role/tabIndex, and the aria* mixin), each reflecting to its content attribute. So a compound child (a tabs trigger, a toggle-group item) resolves its parent and marks active/pressed state in the first server paint, with no hydration flash, and a light-DOM item that sets host attributes inside render() no longer crashes SSR on an undefined dataset. Only tag-name selectors resolve at SSR; class / attribute / descendant selectors stay client-only.
Fixes
re-export the full directive set from the core browser entry (#227) 993d629f
The @webjsdev/core/directives subpath collapses onto the dist browser bundle (built from index-browser.js), which only re-exported unsafeHTML / live, so a built app importing ref / createRef / keyed / guard / cache / until / templateContent / asyncAppend / asyncReplace / watch got "does not provide an export". Both index entries now re-export the full lit-parity directive set so the bundle carries every directive, and repeat is exposed on the /directives subpath in src mode too, matching the documented table in both modes.
ui-tooltip and ui-hover-card now read their delay-duration / skip-delay-duration / open-delay / close-delay config through typed reactive properties (delayDuration etc.) instead of this.getAttribute. The attribute API is unchanged (shadcn parity); the property form is now also available.
cli
v0.10.4
1 change
Features
make webjs check correctness-only; move conventions to CONVENTIONS.md (#225) f9f13e48
webjs check now runs only correctness checks (a crash, a security leak, a build/type-strip failure), unconditionally. The four preference rules (actions-in-modules, one-function-per-action, tests-exist, no-json-data-files) and the package.jsonwebjs.conventions override mechanism are removed; project conventions move to CONVENTIONS.md guidance. The webjs check --rules output and the success line are updated to match.
server
v0.8.9
2 changes
Features
make webjs check correctness-only; move conventions to CONVENTIONS.md (#225) f9f13e48
checkConventions now runs only correctness rules (a crash, a security leak, a build/type-strip failure), unconditionally. The four preference rules (actions-in-modules, one-function-per-action, tests-exist, no-json-data-files) and the package.jsonwebjs.conventions override mechanism (loadConventionOverrides, the opts.rules param) are removed.
Fixes
narrow no-browser-globals-in-render for the SSR shim (#218) 7857a731
Now that the SSR server base shims the attribute / event / attachInternals methods, no-browser-globals-in-render no longer flags them, and it scans willUpdate (which now runs at SSR) in addition to the constructor and render.
core
v0.7.6
2 changes
Features
run willUpdate/hostUpdate at SSR with a server element shim (#218) 7857a731
The SSR walker now runs the pre-render lifecycle (willUpdate, controllers' hostUpdate) and reflects reflect: true properties before render(), and WebComponent's server base is a DOM shim backing the attribute methods, no-op events, and an inert attachInternals. So lit muscle-memory patterns (deriving render state in willUpdate, reading attributes in render, reflecting a property) work server-side instead of crashing or producing a wrong first paint.
Fixes
decode Object/Array attributes at SSR (#221) 02e14621
applyAttrsToInstance JSON-parsed the raw entity-encoded attribute text, so a JSON value in an attribute (every " escaped to ") failed to parse and silently became a string at SSR. It now decodes the entities first, so the attribute form of a rich Object / Array prop renders correctly server-side.
server
v0.8.8
1 change
Fixes
harden auth and websocket against malformed input (DoS) (#215) fd2913a3 * fix: a malformed auth cookie reads as no session instead of crashing
unsign() calls atob via unb64url, which throws a DOMException on non-base64 input, but unlike decodeJwt it had no try/catch. So a malformed or
ui
v0.3.3
1 change
Fixes
dropdown-menu inset is SSR-safe (#208) 54ffb02UiDropdownMenuItem, UiDropdownMenuLabel, and UiDropdownMenuSubTrigger read this.hasAttribute('inset') inside render(), which throws during SSR (the server-side instance has no DOM), so the render was swallowed and the items server-rendered empty, appearing only after client hydration. The render-time attribute read is now a reactive inset boolean property the SSR walker applies before render, so the items server-render their data-inset state correctly.
core
v0.7.5
3 changes
Features
webjs:prefetch event when a speculative fragment is cached (#202) 651d407 The client router now dispatches a webjs:prefetch event on document the instant a prefetched page fragment lands in the cache and becomes consumable (strictly later than the request going out). The detail is { url, key, from: 'prefetch' }, mirroring webjs:navigate, which now also carries from: 'navigate', so one listener can split the two. Use it to instrument prefetch hit rate or gate work on a warm cache.
Actionable SSR errors for browser-only APIs in a component (#208) 54ffb02 When a component touches a browser global (document, window, ...) or an HTMLElement member on this (setAttribute, classList, ...) in its constructor or render(), which the SSR pipeline runs on a bare server-side class, the resulting crash now logs a message naming the offending member and the fix (move it to connectedCallback or a lifecycle hook), instead of a raw document is not defined.
Fixes
shared rich values round-trip through the RPC serializer (#212) ff4332a A Date, typed array, Blob, or File that appears more than once in a single payload (the same instance, which the encoder emits as a back reference) decoded the first copy but threw Dangling reference on the second, crashing any server action or richFetch that returned such a shared value. The decoder now registers every leaf rich type under its id, so shared rich values round-trip and preserve reference identity.
server
v0.8.7
4 changes
Features
WEBJS_ELIDE environment override for the elision switch (#203) 444fb29 A WEBJS_ELIDE env var now overrides the package.jsonwebjs.elide switch: 0 / false / off / no force component elision off, 1 / true / on / yes force it on, and any other value falls through. It is the deploy-time escape hatch to rule elision out while debugging a suspected wrong-strip, without editing committed code.
no-browser-globals-in-render convention rule (#208) 54ffb02webjs check now flags browser globals and HTMLElement members used in a component's constructor or render(), which the SSR pipeline runs on a bare server-side class with no DOM, so they throw at SSR time. The rule names the member and the fix (move it to connectedCallback or a lifecycle hook). It is conservative: only the constructor and render bodies are scanned, only direct references, and string/template content is masked.
Fixes
comments are masked before the elision tag and observation scans (#201) 8b0c8d8 A commented-out custom-element tag, whenDefined, or @event no longer forces a display-only component to ship. The elision analyser now masks comment bodies (keeping string and template content) before its tag, observation, and interactivity scans, so dead code in a comment cannot defeat the optimization.
the transformed-source cache is per request handler (#203) 444fb29 The stripped-TypeScript-plus-elision cache was module-global keyed only on path and mtime, but the cached bytes bake in a handler's elision verdict, so two createRequestHandler instances for the same app with different elision settings (a multi-tenant embedder) served each other's elided source. The cache now lives in per-handler state; single-handler production behaviour is unchanged.
server
v0.8.6
1 change
Fixes
a committed vendor pin now prunes elided deps, so pinned == unpinned (#198) 97583d5 A committed .webjs/vendor/importmap.json was served verbatim, skipping the elision-aware pruning the live-resolve path applies. So a vendor package whose only importer is a display-only (elided) component stayed in the served importmap when pinned but was pruned when resolved live, meaning a pinned app and an unpinned app served different maps for the same source. The pin is now applied verbatim at boot (for a stable build id) and then pruned in ensureReady, once elision is known, to the specifiers still reachable from non-elided modules (prunePinToReachable), so both serve the same map. The build id stays the boot-published hash of the committed pin, so the served map shrinks with no warmup-time build-id drift, and the prune re-runs after every dev rebuild rather than regrowing the full pin.
cli
v0.10.3
1 change
Features
scaffolded apps ship a platform-neutral readiness gate (#195) 8385001 A new app's Dockerfile and compose.yaml now carry a HEALTHCHECK that probes the framework's /__webjs/ready endpoint (503 until the instance is fully warm, then 200). Docker, compose, and most Docker-based platforms honour it, so a cold deploy holds traffic until the app is warm without any per-platform config. The probe is dependency-free (Node 24's built-in fetch). The scaffold AGENTS.md documents /__webjs/ready and the per-platform knob for platforms that read their own config (Railway healthcheckPath, Render healthCheckPath, Fly [checks], k8s readinessProbe).
server
v0.8.5
1 change
Fixes
the core runtime and other static assets no longer wait on the first vendor resolve (#191) aa3eb43/__webjs/core/ (the @webjsdev/core runtime every page boots from), the dev reload client, and downloaded /__webjs/vendor/ bundles were served after the per-request ensureReady() analysis, so on a cold instance they blocked on the first vendor resolve (an unpinned app's api.jspm.io call, a 10s timeout with transient retries stacking toward ~30s). Because the core bundle is on every page's boot path, that stalled first interactivity site-wide whenever an instance was cold. They are now served before ensureReady(), like the health and readiness probes, since they depend on neither the analysis nor the vendor importmap. The /__webjs/core/ path guard also moves from a raw prefix check to a trailing-separator boundary, closing an encoded-slash (..%2f) escape to an @webjsdev/core-prefixed sibling.
cli
v0.10.2
1 change
Fixes
webjs test discovers the documented feature-folder layout; the scaffold test gate moves to CI (#188) bc8e67cwebjs test (server layer) did a flat readdir of test/, so the documented test/<feature>/<name>.test.ts layout ran zero files and a scaffolded app's starter test never ran. It now walks test/ recursively, skips browser/ subfolders (web-test-runner owns those), and gates e2e/ behind WEBJS_E2E=1, finally implementing the documented opt-in in the runner. Scaffolded apps also ship a lighter .hooks/pre-commit (it only blocks commits to main; the test and convention gate runs in a generated .github/workflows/ci.yml instead, so git commit stays fast and the gate cannot be skipped with a local --no-verify), plus a Dockerfile, compose.yaml, and .dockerignore so a new app is deployable out of the box.
server
v0.8.4
1 change
Fixes
elision ships a display-only component that other code observes (#171) f700ddd Display-only elision drops a component's customElements.define when its own render is inert, but missed that OTHER client code can observe the tag's registration. A shipping customElements.whenDefined('the-tag'), a CSS the-tag:defined rule, or an instanceof TheClass check now forces the observed component to ship, so the observation no longer silently fails (whenDefined never resolving, :defined never matching, instanceof always false). The instanceof form maps the class name back to a tag via the component class name. Verdict-safe: it only ever ships more. Dynamic tag strings and external stylesheets stay an author-facing caveat.
core
v0.7.3
2 changes
Fixes
client router no longer intercepts JS-handled links (#157) c70860b The router's click listener ran in the capture phase, ahead of a component's own @click handler, so a link whose handler calls e.preventDefault() was still hijacked and SPA-navigated. The listener is now registered in the bubble phase, so the router sees event.defaultPrevented and leaves the link alone. Plain <a href> links are still SPA-navigated as before.
client router no longer intercepts JS-handled forms (#151) 7d84355 The submit listener had the same capture-phase bug: a form that handles submission in JS (@submit=${e => { e.preventDefault(); ... }}, the live chat and comment boxes) was hijacked by the router, which navigated the page and dropped the just-sent message. The submit listener is now a bubble listener too, so a preventDefaulted form keeps its submission and the router stays out of it.
core
v0.7.4
1 change
Features
Automatic link prefetch in the client router (#160) 7420f60 Internal <a href> links now prefetch on intent (hover, focus, or touchstart) by default, so the destination is usually already in cache by the time the click lands and navigation feels instant. Strategy is controlled per link with a Next-style data-prefetch attribute: intent (default), render (warm as soon as the link renders), viewport (warm when the link scrolls into view), or none to opt out. The aliases true / false / auto map to render / none / intent to match Next's prefetch prop values. Only same-origin GET-idempotent links are eligible; cross-origin links, download, target=_blank, and anchors carrying a rel the framework should not second-guess are left untouched, as is the native <link rel=prefetch> mechanism. Prefetches ride a bounded FIFO queue (concurrency cap, LRU eviction, TTL) and are suppressed under Save-Data / prefers-reduced-data. The prefetch cache is evicted on any mutation (revalidate(), non-GET form submission) so a warmed snapshot can never serve stale post-write content.
server
v0.8.3
2 changes
Fixes
modulepreload hints stay within the servable set (#161) 04f6cef The SSR preload emitter (transitiveDeps) walked the module graph differently from the source-serving authorization gate (reachableFromEntries), so the framework emitted <link rel="modulepreload"> hints for URLs it then 404s. Two causes, both fixed: the preload walk now stops at .server.* boundaries like the gate (a server-only util reached through a server action is no longer preloaded), and the import scanner masks string / template-literal content so an import shown as example code inside an html\\`` template is no longer mistaken for a real dependency. The preload set is now a subset of the servable set by construction.
webjs check no longer leaks a git env var across worktrees (#156) 9a83ea2 The check-ignore lookup inherited a GIT_* environment variable that, in a git worktree, pointed at the parent checkout, so ignore resolution ran against the wrong directory. The lookup now runs with a clean env scoped to the target repo.
cli
v0.10.1
1 change
Features
Scaffolded apps ship a test-coverage gate (#164) 72e4af8webjs create now wires a commit-time gate into every new app so a change cannot ship with the wrong test layers. The scaffold carries a .claude/hooks/require-tests-with-src.sh PreToolUse hook (registered in .claude/settings.json) that blocks a git commit staging source with no accompanying test, and blocks a commit that net-removes test lines, with WEBJS_NO_TEST_GATE / WEBJS_ALLOW_TEST_REMOVAL escape hatches. The same floor is enforced for non-Claude agents through the universal .hooks/pre-commit, and the scaffold AGENTS.md / CONVENTIONS.md spell out unit / browser / e2e / smoke as a per-layer Definition of done. Most webjs apps are built with an AI agent, so the gate reaches end-user projects, not just the framework repo.
cli
v0.10.0
2 changes
Features
webjs vendor command surface for importmap pinning (#105) 4db2360 Brings Rails importmap-rails parity to the CLI. webjs vendor pin [pkg] resolves bare npm specifiers and writes them to a committed .webjs/vendor/importmap.json, with unpin / list to manage it. --from <provider> selects the CDN (jspm default, plus jsdelivr, unpkg, skypack) and persists the choice in the pin file so later update runs stay on the same CDN. webjs vendor audit checks pinned versions against the npm security-advisory endpoint and exits non-zero on any CVE; webjs vendor outdated lists pinned packages trailing their registry latest; webjs vendor update re-resolves the outdated set. --download caches the bundles under .webjs/vendor/ for offline serving.
direct jspm.io vendor resolution (Rails-style no-build) (#89) 988b37b Adds the first webjs vendor pin path and the scaffold wiring behind it, resolving npm dependencies to CDN URLs at serve time with no bundler, so a freshly scaffolded app can pin its vendor dependencies out of the box.
core
v0.7.2
2 changes
Performance
serve the browser runtime as a single bundle (#148) 1b072e1 The dist build no longer code-splits core into per-subpath entries plus shared chunks. It ships one self-contained webjs-core-browser.js that the bare @webjsdev/core specifier and the /directives, /context, /task, and /client-router subpaths all resolve to, so a dist-mode page makes a single cacheable framework request with no chunk-discovery waterfall. /lazy-loader stays its own on-demand file; static / elided pages still ship zero core.
Fixes
never hard-reload against an empty importmap build id (#147) 8853e0a The client router treats an empty or absent data-webjs-build on either side as "version unknown" and stays on a soft swap instead of a full page reload (the importmap-textContent fallback is dropped). A genuine cross-deploy still reloads, since both sides then carry non-empty, differing ids. This stops a runtime-first-boot server's warmup window from hard-reloading mid-interaction and wiping a half-filled form.
server
v0.8.2
1 change
Features
gate readiness on a fully warm instance; publish the pinned build id at boot (#147) 8853e0a/__webjs/ready now returns 200 only after the deterministic analysis AND the first vendor attempt have both completed, so a readiness-gated platform (Railway healthcheckPath, a k8s readinessProbe) admits traffic only once the importmap build id is settled, never mid vendor-resolution. A pinned app (committed .webjs/vendor/importmap.json) reads that map at boot and publishes its build id immediately, so a freshly-deployed pinned instance is detected as a new deploy with zero warmup window. The first vendor attempt is bounded by the jspm timeout, so an offline app still becomes ready shortly after.
Fixes
stabilize the importmap build id across the warmup window (#147) 8853e0a Runtime-first boot resolved vendor lazily, so the data-webjs-build / X-Webjs-Build value changed across the first few responses (empty while warming, then resolved), and the client router treated that drift as a deploy and hard-reloaded, wiping form input on a fresh deploy. The server now advertises a PUBLISHED build id promoted only when the importmap is authoritatively final, and early requests await the first resolve so the first served response already carries the complete map. An empty id is reload-safe.
server
v0.8.1
14 changes
Features
make /__webjs/ready a real readiness gate, add warm-up timing (#141) 440e2d5 /ready now returns 503 until the lazy first-request analysis is warm, then 200, so a Kubernetes readinessProbe holds traffic off a not-yet-warm instance instead of routing the first user request into the cold analysis. /health stays liveness-only. ensureReady also logs a one-line per-pass timing breakdown (graph/scan/gate/actions/middleware/elision/vendor) so a slow first request is diagnosable.
decouple vendor resolution from readiness; request-driven retry (#141) 6fb9c29 Readiness gates on the deterministic analysis only, not vendor resolution, so an offline or partially-unresolvable app still boots. Vendor stays best-effort (pinned reads the committed map; unpinned auto-fetches jspm). A transient failure (network / timeout / jspm 5xx) is re-attempted on the next request, non-blocking, with no internal retry timer: the platform's traffic and probes are the retry loop. A permanent unresolvable (jspm 401 for a private/workspace dep) reports ok and is tolerated. The warm-up is a single best-effort kick.
optional readiness.{js,ts} hook for live dependency health (#141) 568fe87 An app may default-export an async check from readiness.{js,ts} at its root; once the analysis is warm, /ready runs it on every probe and reports 503 unready if it returns false or throws, so a readinessProbe can gate on live dependency health (a DB ping) that static analysis cannot see.
self-warm the lazy analysis in the background after listen (#141) 72e7a15 Boot stays instant, but a real first request no longer has to pay the analysis latency. createRequestHandler exposes warmup() (a caught, idempotent call to the single-flighted ensureReady), and startServer fires it fire-and-forget once the HTTP server is listening, so the module graph, scan, gate, action index, middleware, elision, and vendor map are warm before the first real request, hiding the analysis latency for long-lived processes.
Performance
evict deleted-file entries from the parse/scan caches on rebuild (#141) e31db37 The incremental-rebuild parse and scan caches only ever overwrote per-path entries, so a long dev session with renames/deletes accumulated dead entries. buildModuleGraph and scanComponents now drop any cache key under the app dir not seen in the latest walk, bounding cache size to live files.
defer pure-RPC server-module loads off boot (#141) 88efccd buildActionIndex hashed every .server file AND imported it, executing every server module at boot (firing Prisma init, DB connects, and any module-init side effect). Only expose() needs eager loading (it registers a REST route the router must know before a request hits it). So hash all server files for the RPC dispatch index, but eagerly load only the ones that reference expose(); every other server module imports on its first call.
skip the boot bare-import scan when a vendor pin file exists (#141) 19ca11a resolveVendorImports read the pin file but only AFTER dev.js had already run the whole-app scanBareImports walk, whose result the pin path discards. Move the scan behind a thunk that resolveVendorImports invokes only on the unpinned path, so a pinned app (the recommended posture) does zero vendor static analysis, reading only the committed importmap file.
defer unpinned vendor resolve off boot to first request (#141) 7301fa8 Boot no longer resolves the vendor import map at all. A memoized, single-flighted ensureVendor() runs on the first request (before any SSR head emits the importmap or its build hash): a pinned app pays only a file read, an unpinned one pays the scan + jspm.io call, both off the boot path.
compute elision lazily on first request, not at boot (#141) 645285b The analyzeElision fixpoint no longer runs at boot. A memoized, single-flighted ensureElision() computes the elidable-component and inert-route sets on the first request (fast, in-memory over the module graph, no network or module execution) before any SSR or module serve reads them. Boot does no elision work at all.
defer all whole-app analysis to a lazy first-request memo (#141) cb8f00c Boot now builds only the route table (a cheap directory scan, kept eager so routing / Early Hints / WebSocket lookups are always available). The module graph, component scan + registry prime, browser-bound gate, action index, middleware, elision, and vendor map are consolidated into one memoized, single-flighted ensureReady() that runs on the first request, so boot reads no app source, executes no server module, walks no graph, and makes no network call.
mtime-keyed parse/scan caches make rebuilds incremental (#141) 259444a buildModuleGraph and scanComponents re-read every file's content on each rebuild. Add an mtime-keyed cache to each: a rebuild re-walks the (cheap) directory tree but reuses the cached import set / component list for any file whose mtime is unchanged, reading + regex-parsing only the files that actually changed, so large apps get near-instant rebuilds.
make the elision render-rule analysis linear (O(N+E)) (#141) e47346f Benchmarking runtime-first boot surfaced an O(N^2)/O(N^3) blowup in the elision analyser on deep component render chains: ~13s at 10k components and out-of-memory at 20k. Three super-linear pieces, all now linear: emittableTags follows helper edges only, the closure-client-work check is one reverse-BFS, and the fixpoint is a worklist over reverse-import edges. 10k went from 13.4s to 0.5s; 20k from out-of-memory to 1.0s. Verdict-safe (it never under-ships).
Fixes
widen lazy-index expose detection to aliased imports (#141) 962752f The lazy action index matched only the literal expose(, so an aliased import (import { expose as exp }; exp(...)) was never eagerly loaded and its REST route silently 404'd while the RPC stub kept working. Match the bare expose identifier instead, so the route registers regardless of how the import is named.
key the incremental parse/scan caches by mtime AND size (#141) f8b6df8 The mtime-keyed caches added for incremental rebuild could, on a coarse- resolution filesystem or a sub-tick edit, miss a content change that left mtime unchanged. Adding the file size to the key catches any length- changing edit even when mtime collides. Cheap correctness hardening; the size discriminator catches the rare same-mtime length-changing edit.
server
v0.8.0
11 changes
Breaking
jspm.io direct vendor (Rails-style no-build) (#89) 988b37b * feat(server): tighten bare-import scanner to exclude server-only files and false positives
The vendor scanner picked up server-only imports from contexts that never reach the browser, generating spurious vendor pipeline work
Closes the four real-Rails gaps the parity review surfaced:
auto-load <appDir>/.env at server boot (#107) a9a97a4 Tracker #37. Scaffolded apps ship a .env.example that users copy to .env, but webjs dev / webjs start never read the file. Booting the SaaS scaffold failed with createAuth() requires a 'secret' because lib/auth.server.ts calls `createAuth({ secret:
publish-time esbuild bundles in dist/ alongside src/ (#117) 6dd0360 * feat(core): publish-time esbuild bundles in dist/ alongside src/
Acceptance criterion 1 + 2 of #113.
split browser-only entry from index, drop server modules (#128) 0c8cb31 * feat(core): split browser-only entry from index, drop server modules
@webjsdev/core's index.js re-exported renderToString, renderToStream, expose, getExposed, and setCspNonceProvider
add display-only component elision analyser0eb958c The analyser decides whether a component module can be elided from the browser (SSR'd HTML is the complete output, no client work). It is a conservative denylist of interactivity signals: anything unrecognised ships. The signal lists are the single source of truth, enforced by a
elide display-only component imports from served modules14e0bd1 Compute the elidable-component set at boot and on rebuild, then strip side-effect imports of those components from served browser modules so the browser never downloads their JS. The modulepreload hint was only a waterfall optimisation; the static import in page/component source is
drop elided components from preloads and importmap8817223 Filter display-only components out of modulepreload hints (and the subtree reachable only through them, via a skip set on transitiveDeps), and exclude vendor specifiers imported only by elided components from the importmap by scanning bare imports after the elision set is known.
add webjs.elide: false opt-out switch Project-level escape hatch. Setting { "webjs": { "elide": false } } in package.json skips the elision analysis entirely (everything ships, like before the feature). Pure opt-out, re-read on every rebuild, default on.
Fixes
detect module-scope client work by an allowlist of safe top-level forms Elision's "does this module do client work at load" check is an allowlist (top level may only declare and register; any other top-level call, new, dynamic import(), or await ships) instead of a denylist of browser global names. This closes false-elision holes (a module-scope fetch, new WebSocket, or dynamic import() was wrongly elided) and does not rot as browsers add APIs.
hand-rolled lexer tracks regex literals and nested template interpolationredactStringsAndTemplates is now a proper lexer. Regex literals are delimited by the standard regex-versus-division rule and their bodies blanked, and template literals are tracked with full ${...} nesting, so a quote or brace inside a regex, or a nested ` html...${html...}`, can no longer desync the scanner and cause a false elision verdict.
template-literal-aware scanner eliminates 9 false positives (#109) f411477 * fix(check): redact template literals before scanning to remove 9 false positives
The check rules scan source via regex. Docs pages render code examples inside html... template literals, and test files write fixture
scope source serving to the browser-bound module graph (#111) 07271fb * fix(server): scope source serving to the browser-bound module graph
Webjs's dev server previously served any file under appDir whose path ended in a known extension (.js, .ts, .json, .svg, …) with only
add trustProxy option; default to socket-stamped IP (#116) 3ddb5ad * fix(rate-limit): add trustProxy option; default to socket-stamped IP
Closes #114.
cli
v0.9.1
1 change
Fixes
sync scaffold dark mode so ui-* components follow the theme (#101) f0a1fa1 * fix(cli): sync scaffold dark mode so ui-* components follow the theme
The default scaffold shipped two unsynced theme systems: the editorial chrome tokens (--fg/--bg) toggled via a data-theme attribute, while
cli
v0.9.0
2 changes
Breaking
Default webjs dev / webjs start port is now 8080 (was 3000) (#97) 7c44e6e
3000 is heavily contested locally (Next.js, CRA, Express, Flask all default there), so newly scaffolded apps now serve on 8080. process.env.PORT still wins, so deploys that inject PORT (Railway, Fly, Heroku, etc.) are unaffected.
Migration: to keep the old port, set PORT=3000 (or pass --port 3000) when running webjs dev / webjs start. Scaffolded apps need no change.
Fixes
Resolve the @webjsdev/ui registry via the module resolver, not path math (#95) f01059b
webjs create located the UI registry with __dirname/../../ui/packages/registry, which assumed @webjsdev/ui was hoisted as a sibling of @webjsdev/cli. That broke on non-hoisted layouts (pnpm's isolated linker, npm install --install-strategy=nested, some CI setups), failing the scaffold. It now resolves the registry through require.resolve, so webjs create works regardless of install layout.
server
v0.7.3
1 change
Changes
startServer default port and OAuth fallback origin moved to 8080 (#97) 7c44e6e
startServer({ appDir }) without an explicit port now defaults to 8080 (was 3000), matching the new @webjsdev/cli default, and the OAuth no-request fallback origin moved from localhost:3000 to localhost:8080. process.env.PORT and an explicit port option still win. The recommended createRequestHandler embedding path is unaffected (the host server owns the port there).
ui
v0.3.2
1 change
Fixes
include packages/registry in npm package filesa23b43e
cli
v0.8.6
1 change
Fixes
fail loudly when @webjsdev/ui registry is missing at scaffold time6ac4a0d The scaffold reads UI component sources directly from the installed @webjsdev/ui's packages/registry/ directory, then generates an app/page.ts that imports the copied files. Until the previous commit, if the registry wasn't on disk, the copy helpers silently no-op'd and
cli
v0.8.5
4 changes
Features
auto-install dependencies after webjs create (#72) webjs create <name> runs <pm> install inside the new directory by default. Pass --no-install to opt out. The package manager is detected from npm_config_user_agent, so pnpm / yarn / bun users get their own. The library-level scaffoldApp(...) default stays opt-in (install: true required) so tests and programmatic callers remain side-effect-free.
scaffold's next-steps banner reordered so the run command lands last (#72) Previously the "Next steps:" block printed BEFORE the long AI-agent guidance, pushing the actionable cd <name> && <pm> run dev line off the visible terminal. New order: AI guidance prints first (long reading material), install runs, then the run command lands as the final output. Single copy-paste line.
Fixes
scaffold's next-steps banner points at npx webjsdev ui ... instead of the broken npx webjs ui ... (#72) The bare webjs npm name is owned by an unrelated package, so npx webjs <cmd> would fetch THAT package outside a scaffolded project. Switched both banner lines to the unscoped CLI alias webjsdev, which resolves to the same webjs binary via npx's single-bin fallback.
Breaking
0.8.2's wjs bin alias was reverted in 0.8.3+ (#72) cli@0.8.2 briefly shipped with both webjs and wjs bin entries. cli@0.8.3 and later ship only webjs. Anyone who installed cli@0.8.2 and started typing wjs <cmd> should switch to webjs <cmd>. Behaviour is identical, only the command name differs. The unscoped webjsdev package now provides the no-scope global install path; the wjs namespace was blocked by npm's name-similarity filter.
See also
@webjsdev/cli@0.8.5 ships in lockstep with two new npm packages introduced in PR #72:
create-webjs for npm create webjs@latest my-app scaffolding
webjsdev as the unscoped CLI mirror for npm i -g webjsdev
Both auto-version-bump in lockstep with cli via .github/workflows/release.yml's lockstep step.
ts-plugin
v0.4.1
1 change
Breaking
Rescope from @webjskit/ts-plugin to @webjsdev/ts-plugin (#62) The npm scope now matches the canonical webjs.dev domain (and the @webjsdev org we own on npm). Functionally identical to the prior @webjskit/ts-plugin@0.4.0 publish, no plugin behavior change. Update your tsconfig.jsonplugins entry from @webjskit/ts-plugin to @webjsdev/ts-plugin and reinstall. The legacy @webjskit/* packages stay installable, but will be marked deprecated on the registry to redirect new installers here.
ui
v0.3.1
1 change
Breaking
Rescope from @webjskit/ui to @webjsdev/ui (#62) The npm scope now matches the canonical webjs.dev domain (and the @webjsdev org we own on npm). Functionally identical to the prior @webjskit/ui@0.3.0 publish, no component or CLI behavior change. The webjs ui add command now writes imports from @webjsdev/core into added components. Update existing lib/utils/ui.ts and any other manually-imported references from @webjskit/ui to @webjsdev/ui. The legacy @webjskit/* packages stay installable, but will be marked deprecated on the registry to redirect new installers here.
core
v0.7.1
1 change
Breaking
Rescope from @webjskit/core to @webjsdev/core (#62) The npm scope now matches the canonical webjs.dev domain (and the @webjsdev org we own on npm). Functionally identical to the prior @webjskit/core@0.7.0 publish, no API or runtime change. Migrate by find-replacing every @webjskit/ reference with @webjsdev/ in source, imports, and package.json dependency keys. The legacy @webjskit/* packages stay installable, but will be marked deprecated on the registry to redirect new installers here.
server
v0.7.2
1 change
Breaking
Rescope from @webjskit/server to @webjsdev/server (#62) The npm scope now matches the canonical webjs.dev domain (and the @webjsdev org we own on npm). Functionally identical to the prior @webjskit/server@0.7.1 publish, no API or runtime change. Migrate by find-replacing every @webjskit/ reference with @webjsdev/ in source, imports, and package.json dependency keys. The legacy @webjskit/* packages stay installable, but will be marked deprecated on the registry to redirect new installers here.
cli
v0.8.1
1 change
Breaking
Rescope from @webjskit/cli to @webjsdev/cli (#62) The npm scope now matches the canonical webjs.dev domain (and the @webjsdev org we own on npm). Functionally identical to the prior @webjskit/cli@0.8.0 publish, no CLI behavior change. Existing global installs (npm i -g @webjskit/cli) keep working; new users should npm i -g @webjsdev/cli. Scaffold templates now emit @webjsdev/ dependencies. The legacy @webjskit/ packages stay installable, but will be marked deprecated on the registry to redirect new installers here.
ui
v0.3.0
4 changes
Breaking
signals replace this.state / setState across the stack (#43) 6e50ae6 * feat(core): signal primitive (TC39 Stage-1 shape, no runtime deps)
Hand-rolled signal/computed/effect/batch in plain JS with JSDoc. Surface mirrors the TC39 Signals proposal so when it lands in
Features
env-driven sibling URLs with localhost dev fallbacks (#42) f433efc The UI website hardcoded https://webjs.dev and https://docs.webjs.dev as the defaults for its header + footer links. Local dev navigated off the localhost dev cluster as soon as you clicked the Webjs / Docs links, which broke the flow when working across all three apps simultaneously.
per-package per-version changelog + /changelog page + auto-enforce on version bumps (#49) 3e5e573 * feat(changelog): per-package per-version changelog + backfill from git
Introduce a changelog/ directory at the repo root with one md file per (package, version) tuple:
Fixes
align @webjskit/* deps to workspace versions (#35) e2cb080 ui-website pinned @webjskit/server: ^0.5.2, @webjskit/core: ^0.4.1, @webjskit/cli: ^0.5.2 while the monorepo workspaces are at 0.6.0, 0.5.0, 0.6.0. npm therefore installed published 0.5.x tarballs into packages/ui/node_modules/@webjskit/* and skipped the workspace
core
v0.7.0
2 changes
Breaking
signals replace this.state / setState across the stack (#43) 6e50ae6 * feat(core): signal primitive (TC39 Stage-1 shape, no runtime deps)
Hand-rolled signal/computed/effect/batch in plain JS with JSDoc. Surface mirrors the TC39 Signals proposal so when it lands in
Fixes
filter framework records in light-DOM slot observer (#44) 272d417 * fix(core): filter framework records in light-DOM slot observer
The slot host's MutationObserver could not distinguish renderer-driven childList mutations (createInstance / child-part template swaps) from
cli
v0.8.0
1 change
Breaking
signals replace this.state / setState across the stack (#43) 6e50ae6 * feat(core): signal primitive (TC39 Stage-1 shape, no runtime deps)
Hand-rolled signal/computed/effect/batch in plain JS with JSDoc. Surface mirrors the TC39 Signals proposal so when it lands in
server
v0.7.1
1 change
Fixes
stop sending immutable cache-control on /__webjs/core/* (#38) f6bd2d6 The /__webjs/core/* paths are un-versioned: every bump of @webjskit/core ships different bytes at the same URL. Sending cache-control: public, max-age=31536000, immutable instructed edge CDNs (Cloudflare in our case) to pin the prior bytes for up
core
v0.6.0
4 changes
Features
SSR property bindings via data-webjs-prop-* side-channel2c4c98e A property hole (.prop=${val}) on any element used to be dropped silently at SSR. The data was lost between page function and child component, breaking the natural "fetch data, pass to component" pattern unless authors manually JSON.stringify into
client hydrates data-webjs-prop-* attributes, then stripse8e4383 Completes the SSR property-binding round-trip. The server emits data-webjs-prop-<kebab>="<wire-encoded>" for each .prop=${val} hole in PR-WIP commit 2c4c98e. This commit teaches WebComponent to consume those attributes on the browser side.
skip property-attribute emission for native elements + decode HTML entities on server walker reada3e1132 Two related fixes to the property-binding SSR path.
1. Native-element guard. Property bindings on tags without a hyphen (<input .value=${v}>) used to emit a data-webjs-prop-value
server
v0.7.0
6 changes
Features
drop webjs.conventions.js + legacy fallbacks, only support pkg.webjs.conventions81bdf7c loadOverrides now reads exactly one place: package.json "webjs": { "conventions": { … } }. Returns {} when the key is absent (every default rule active). Removes the old top-level "conventions" fallback, the webjs.conventions.js file path, and
WEBJS_PUBLIC_* env vars accessible as process.env.* in browser60eed0e Adds an SSR-injected inline script that defines window.process.env with all server-side env vars whose name starts with WEBJS_PUBLIC_, plus NODE_ENV based on dev/prod mode. Counterpart of Next.js's NEXT_PUBLIC_ convention, without a build step.
no-server-env-in-components lint rulea0dc926 Flags process.env.X reads in component files where X is not WEBJS_PUBLIC_* and not NODE_ENV. The SSR shim only exposes those two categories to the browser; any other read either leaks a secret into the SSR'd HTML or reads as undefined
split .server.ts source-protection from 'use server' RPC marker41a61bb The two markers are now complementary instead of interchangeable:
.server.{js,ts} path-level boundary: file router refuses to serve the source to the browser.
add use-server-needs-extension lint rule7e543a6 The rule catches files that declare 'use server' at the top but lack the .server.{js,ts,mts,mjs} extension. Under the two-marker convention, the directive alone is silently ignored: the file serves to the browser as plain source, exports are not registered as RPC,
Fixes
remove no-server-imports-in-components lint rule entirely2bca0dd The convention 'server-only code goes in .server.ts, route.ts, or middleware.ts; never in pages, layouts, or components' is real and important, but pure static lint enforcement is the wrong mechanism:
cli
v0.7.0
9 changes
Features
webjs check --rules shows enabled/disabled state per projectff0adcb Reads the project's overrides via loadConventionOverrides(), then annotates each rule line as [enabled] or [disabled by override]. Header text states the default-on invariant explicitly so AI agents reading the output cannot misinterpret it.
nudge-uncommitted hook ships in every scaffolded appfd600ca Mirrors the framework-repo hook into the scaffold templates so end-user webjs apps inherit the same commit-frequency enforcement out of the box. Three pieces:
pre-commit runs webjs test + webjs check11068f7 Tool-agnostic git-level enforcement. Fires on every commit regardless of agent (Claude, Cursor, Windsurf, Copilot, human). Blocks commits that fail tests or convention checks.
Gemini CLI nudge-uncommitted hook4c21aa8 Adds the same commit-frequency enforcement to scaffolded apps for Gemini CLI users. The hook output shape (hookSpecificOutput. additionalContext) is identical to Claude Code's, so the body is nearly a copy of the Claude version.
Cursor nudge-uncommitted hook5eb459c Cursor 1.7+ ships a hooks system at .cursor/hooks.json. The afterFileEdit event fires after Cursor's edit/write tools and accepts a top-level additional_context field (snake_case) to inject a soft reminder into the agent's context.
block-prose-punctuation hook ships in every scaffolded appc63dacb Mirrors the framework-repo prose hook into scaffolded apps so end users get the same enforcement of AGENTS.md invariant 11 (no em-dashes, no hyphen-as-pause, no semicolon-as-pause, no xyz():-then-prose). The rule is already documented in
OpenCode commit-nudge plugin2ee5e15 OpenCode does not have shell-script hooks; it uses TS plugin modules loaded as-is by Bun. This plugin is the counterpart of the .claude/, .gemini/, .cursor/ hooks already in the scaffold.
First Tier-2 component converted from extends Base to extends WebComponent. This is the pattern-locking conversion; remaining
Fixes
drop guard-main-merge hook so end users can merge to main locally9b878a4 This hook prompted on every 'git merge' and every 'git push ... main' through Claude's Bash tool. That matches the framework dev's preference for a strict GitHub-PR-only workflow, but it is the wrong default for end users. Scaffolded webjs apps may use GitLab, Bitbucket, plain git,
ui
v0.2.0
6 changes
Features
add tier classifier for registry itemsf263510 New _lib/tier.ts module exports TIER_2_NAMES (the 12 stateful custom elements), tierOf(item), and splitByTier(items). Kept outside the *.server.ts RPC-stub rewrite path so the helper can be imported from any context.
split homepage component grid by tier7a7fc2b The 'All components' section now renders two grouped grids — Tier 1 class helpers (20) above Tier 2 custom elements (12) — with intro copy clarifying the composition model and a hint to pick Tier 1 by default. Uses splitByTier() from the helper added in f2ba5c4.
split docs sidenav components list by tier542e638 Sidenav now renders two grouped sections — 'Tier 1 · Class helpers' (20 components) and 'Tier 2 · Custom elements' (12 components) — with per-section counts. Each section's items are alphabetical within the tier. Uses the splitByTier() helper from f2ba5c4.
native-HTML rewrite of stateful primitives + shadcn-API sweep (#5) f6a9dbf Eight registry components now lean on native HTML primitives instead of hand-rolled JS. Three drop their custom elements entirely; five keep a thin custom-element wrapper around the platform feature.
First Tier-2 component converted from extends Base to extends WebComponent. This is the pattern-locking conversion; remaining
Fixes
rewrite registry '../lib/utils.ts' imports on addaed0f53 Registry components import cn from '../lib/utils.ts' (matching the registry's own layout, where components live one level beside the utils file). When 'webjs ui add <name>' fetches a component over HTTP and writes it to the user's components/ui/<name>.ts, the
core
v0.5.0
5 changes
Features
light-DOM slot runtime foundation (slot.js)b6cc3e8 First slice of the light-DOM slot work. Adds the standalone runtime file owning the slot semantics. Subsequent commits wire it into the compiler, component lifecycle, and SSR pipeline.
slot part in compiler + client renderer + fallback swap hook3258888 Second slice. Wires light-DOM <slot> elements through the existing template compile, bind, apply, and clear pipeline using a new SLOT part kind. Adds the small slot.js hook that swaps fallback content into the slot when projection state is "fallback".
slot lifecycle in WebComponent + per-instance fallback clonec69615b Third slice. Wires the slot runtime into the WebComponent lifecycle and refines render-client.js's slot bind so fallback content is captured once at compile time and cloned freshly per instance.
SSR slot substitution in injectDSD595da6a Fourth slice. Upgrades injectDSD to project authored children into <slot> positions during server-side rendering for light-DOM WebComponents, with full parity to the client-side projection rules and the shadow-DOM <slot> spec.
Fixes
6 browser failures fixed, all 26 browser tests passe99c33a Four real bugs surfaced in the browser test suite and are fixed:
1. captureAuthoredChildren ran on every connectedCallback, including on re-connection of a previously-mounted element. The second
server
v0.6.0
2 changes
Features
hybrid TS stripper (Node strip-types + esbuild fallback)29f1645 Server-side .ts imports now use Node 24+'s default type-stripping natively. The module.register('./esbuild-loader.js') hook and the loader file itself are deleted: SSR and the test runner both pick up .ts files without any framework bootstrap.
erasable-typescript-only rule + ship erasableSyntaxOnly in tsconfigs490372a Adds compilerOptions.erasableSyntaxOnly to: the scaffold tsconfig generator (packages/cli/lib/create.js), the blog example, the docs site, the marketing site. TypeScript 5.8+ rejects enum, namespace with values, constructor parameter properties, legacy decorators
cli
v0.6.0
1 change
Features
erasable-typescript-only rule + ship erasableSyntaxOnly in tsconfigs490372a Adds compilerOptions.erasableSyntaxOnly to: the scaffold tsconfig generator (packages/cli/lib/create.js), the blog example, the docs site, the marketing site. TypeScript 5.8+ rejects enum, namespace with values, constructor parameter properties, legacy decorators
core
v0.4.1
2 changes
Fixes
preserve cached snapshot on popstate; manual scrollRestoration5a3b684 Two bugs combined to break back-button scroll restoration:
1. snapshotCurrent(location.href) at performNavigation entry ran on EVERY navigation including popstate. But on popstate, the browser
scroll to top on cache-miss popstate42142c2 Companion to the previous commit. Setting history.scrollRestoration = 'manual' takes the browser out of the scroll-restoration game on back/forward — which is great when we have a cached snapshot (we restore exactly where the user left off) but leaves the cache-miss
server
v0.5.1
1 change
Fixes
honor X-Forwarded-Proto/Host when building ctx.url952d414 webjs apps deployed behind a TLS-terminating reverse proxy (Railway, Fly, Render, Vercel, Cloudflare, nginx, Caddy, Traefik — the typical no-build deployment topology) receive plain HTTP at the container with X-Forwarded-Proto: https + X-Forwarded-Host: <public-domain> headers
server
v0.4.0
1 change
Features
rule reactive-props-use-declare flags class-field foot-gun4ef105f A reactive property listed in static properties = { … } MUST be typed via declare propName: Type and have its default set inside constructor(). A plain class-field initializer (propName = value or propName: Type = value) compiles under
ts-plugin
v0.3.0
1 change
Features
type-check <webjs-tag attr=\${expr}> interpolations306bcb4 Extend the plugin with a fourth resolver: for every \${expr} interpolation in attribute-value position of a reachable webjs tag, look up the matching declare attr: T field on the component class and assignability-check the expression's type against T using the
ts-plugin
v0.2.0
2 changes
Features
rebrand webjs-plugin → @webjskit/ts-plugin, ready for npmcf89060 Matches the scope of the three published packages (@webjskit/core, @webjskit/server, @webjskit/cli). The ts-* naming suffix mirrors the tsserver-plugin convention (cf. ts-lit-plugin).
suppress lit-plugin "unknown tag/attr" + complete attrs25f9863 ts-lit-plugin doesn't recognise webjs components — they're registered at runtime via Class.register('tag') with no @customElement decorator or HTMLElementTagNameMap augmentation — so it fires "Unknown tag" / "Unknown attribute" diagnostics on every webjs element used inside
server
v0.3.1
1 change
Fixes
hoist <link> from layout body into <head>cfa4827 Layouts emit <link rel="icon">, <link rel="stylesheet">, etc. in their template body, but the SSR pipeline wraps that body in <div data-layout="..."> before the existing hoister ran — so the hoister (a) never saw <link>, and (b) couldn't reach past the wrapper
core
v0.3.0
1 change
Features
drop superjson, ship built-in ESM rich-type serializer4efefce Replaces superjson with a pure-ESM, dependency-free serializer in @webjskit/core (packages/core/src/serialize.js). Tagged-inline wire format, async sync API. Single async stringify/serialize so AI agents can't pick the wrong variant; parse/deserialize stay sync.
server
v0.3.0
1 change
Features
drop superjson, ship built-in ESM rich-type serializer4efefce Replaces superjson with a pure-ESM, dependency-free serializer in @webjskit/core (packages/core/src/serialize.js). Tagged-inline wire format, async sync API. Single async stringify/serialize so AI agents can't pick the wrong variant; parse/deserialize stay sync.
cli
v0.3.0
1 change
Features
drop superjson, ship built-in ESM rich-type serializer4efefce Replaces superjson with a pure-ESM, dependency-free serializer in @webjskit/core (packages/core/src/serialize.js). Tagged-inline wire format, async sync API. Single async stringify/serialize so AI agents can't pick the wrong variant; parse/deserialize stay sync.
core
v0.2.0
43 changes
Features
add essential directives (unsafeHTML, live) with renderer integration3d8a253 Only three directives in webjs — each solves a problem with no native alternative. unsafeHTML for trusted raw HTML, live for input value dirty-checking, repeat (already existed) for keyed lists. Both client and server renderers now process directive markers correctly instead
add Context Protocol and Task controllerd68b207 Context: createContext, ContextProvider, ContextConsumer for cross- component data sharing without prop drilling. Uses W3C Context Protocol (DOM events, crosses shadow DOM).
auto-vendor npm deps, module graph, lazy loading266d041 Auto-vendor: scan client imports for bare specifiers, bundle via esbuild, serve at /__webjs/vendor/<pkg>.js, auto-populate import map.
Module graph: build dependency graph at startup, emit transitive
webjs test, webjs check, webjs create with AI guardrails6e85e5d - webjs test: discovers and runs unit + E2E tests - webjs check: validates app against 6 convention rules - webjs create: scaffolds opinionated project with CONVENTIONS.md, cross-agent configs (.cursorrules, .windsurfrules, copilot-instructions),
light DOM SSR + hydration — zero flash, mixed shadow/light DOMcf155d7 SSR fix: light DOM components (static shadow = false) now have their render() called during SSR. Content is injected directly as children with a <!--webjs-hydrate--> marker (no DSD template wrapper).
light DOM tests, blog migration, auto-scoped styles75f6d17 Tests: - test/light-dom-ssr.test.js: 5 unit tests for SSR (light DOM content, hydration marker, shadow DSD, mixed page, async render) - test/browser/light-dom-hydration.test.js: 3 browser tests (hydration
auto-scope ALL CSS selectors for light DOM componentsbdb3280 scopeCSS() now prefixes every CSS rule with the component tag name: input { color: red } → comments-thread input { color: red } :host { display: block } → comments-thread { display: block }
support mixed attribute interpolation in client renderer893c99f Quoted attributes with embedded holes (class="static ${dynamic}") previously dropped the dynamic portion on client re-render. The renderer now tracks these as 'attr-mixed' parts that reconstruct the full attribute value from static pieces + dynamic values on
default components to light DOM, shadow DOM is opt-in01b55c2 Change static shadow default from true to false. Components now render to light DOM by default. Set static shadow = true to opt into shadow DOM with adoptedStyleSheets.
auto-wrap layout output with data-layout for client router306c512 The SSR pipeline now automatically wraps the outermost layout's rendered output in <div data-layout="LAYOUT_ID">. Users no longer need to add data-layout manually — the framework handles it.
SSR pages default to no-store, caching is opt-in5a4468c SSR responses now send Cache-Control: no-store by default. Pages are dynamic — the developer opts in to caching explicitly via metadata.cacheControl (e.g. 'public, max-age=60'). This ensures client-side navigation always gets fresh content without needing
typed component props via defineComponent + .d.ts overlay9f0b09a The existing static-properties pattern carried no compile-time type information, so authors had to duplicate every field as declare x: T for editor intellisense. This adds a TypeScript overlay plus a tiny runtime helper so the descriptor map is the single source of truth for
Class.register() — drop the import.meta.url argument283c2b5 Authors no longer write Counter.register(import.meta.url); the call is just Counter.register(). Module URLs (used by SSR to emit <link rel=modulepreload> hints) are derived server-side at boot by scanning the app tree for `class X extends WebComponent { static tag =
adopt standard customElements.define — drop static tag + .register()bb61186 The web-standard registration convention (same shape Lit uses for its non-decorator form):
class Counter extends WebComponent { ... }
Counter.register('my-counter') as the idiomatic registrationa0f1f68 Adds a static register(tag) method on WebComponent. Delegates to the internal registry (which in turn calls customElements.define / the server shim), so every registration still lands in the native customElements map and webjs's mirror map. Authors write:
rename core package webjs → @webjs/core for npm publish49e2d6b The unscoped webjs name is already taken on npm (webjs@0.6.1, unrelated project). Moving to the @webjs scope lets us publish @webjs/core, @webjs/server, and @webjs/cli together.
rescope @webjs → @webjskit (org name 'webjs' unavailable on npm)316e2be The @webjs organization is not available on npm (likely reserved because the unscoped webjs package is held by an unrelated project). Switching to the @webjskit scope, which we own.
Fixes
DSD injection regex now handles slashes inside attribute valuesc792b6e Bug: <auth-forms then=\"/dashboard\"> rendered as an EMPTY custom element with no shadow root — the sign-in and sign-up pages appeared blank. Root cause: the DSD-injection regex in render-server.js used [^>/]* to bound the attribute section, which stopped matching the
quoted-attr interpolation no longer crashes client re-rendersdf2a952 Bug: clicking the 'Sign up' tab on /login did nothing. The <auth-forms> component used a hole inside a quoted attribute — \autocomplete=\"\${mode === 'login' ? 'current-password' : 'new-password'}\"\ — and the client compiler was recording a \{ kind: 'attr', path: [] }\ part
boolean attribute SSR coercion — empty value means true016d5b5 Bug: <comments-thread ?signed-in=\${true}> rendered the comment form as "Sign in to comment" instead of the input+Post form, even when the user was logged in.
filter DSD template from client-navigation swap + update rubrica026010 Client router: when swapping the layout shell's light-DOM children on navigation, the new body from DOMParser contains a <template shadowrootmode="open"> element (the SSR's Declarative Shadow DOM). DOMParser doesn't process DSD, so it stays as a regular DOM element.
flicker-free navigation via View Transitions + visibility fallback5f5553c The blog's top navbar flickered during client navigation because replaceChildren on a custom element with a shadow root and <slot> triggers slot redistribution — the browser briefly renders an empty <main> between removing old children and inserting new ones.
eliminate navbar flicker during client navigation4f213d1 Two fixes working together:
1. Client router: changed swap strategy from replaceChildren (empties slot, then fills — one-frame empty state) to append-then-remove
layout-aware navigation — only swap page content, never touch layout5875ed6 Root cause: the client router was calling mergeHead() (which removes/adds <link>, <script> tags in <head>) and reactivateScripts() (which clones <script> elements in <body>) on every navigation. These DOM mutations triggered browser style recalculation and brief repaints of the layout
client router now intercepts clicks inside shadow DOMd89e7bb ROOT CAUSE FOUND: the navbar flicker was a full browser navigation, not a rendering bug. The client router's click handler used e.target to find the <a> element, but click events from inside shadow DOM are retargeted — e.target points to the shadow HOST (<blog-shell>), not the <a> inside
use DSD-aware parsing in client router991d8d0 Neither innerHTML nor DOMParser process Declarative Shadow DOM — custom elements navigated to via the client router had no shadow roots, losing their layout/sidebar/styles entirely.
theme toggle TDZ, light theme default, client router View Transitions race08312a9 - Move ICONS before register() to fix temporal dead zone error on components in light DOM (website theme toggle broken) - Default to light theme across all three apps - Dark theme accent hue aligned to match light theme (55 amber)
rename data-webjs to data-webjs-styles-for — self-documenting47b5168 The attribute on light DOM <style> elements now clearly says what it does: identifies which component owns the style block. Prevents duplicate injection. Added JSDoc for AI agents: do not set manually.
remove auto-scope CSS hack, clean shadow/light DOM conventionf91ceb0 Shadow DOM = scoped styles (static styles = css, adoptedStyleSheets). Light DOM = global styles (Tailwind, global CSS, no static styles). Pick one. Don't fake shadow DOM scoping with regex.
warn when static styles used with light DOM (silently ignored before)7bcb656 static styles + static shadow = false is a mistake — adoptedStyleSheets requires a shadow root. Now logs a clear warning telling the developer to use global CSS or <style> in render() instead.
ensure custom elements upgrade after client-side navigationb357e56 Document.parseHTMLUnsafe() creates elements in a detached document with an empty customElements registry. When those nodes are moved to the live document via replaceChildren, Chromium can silently skip the custom element upgrade — leaving _renderRoot null and connectedCallback unfired.
inject DSD with inline styles for nested custom elementsb012c38 The SSR pipeline's injectDSD() was not recursively processing custom elements nested inside other components' shadow DOM. A <theme-toggle> inside <blog-shell>'s render output was emitted as an empty element with no DSD template — no styles, no content until JS upgraded it.
traverse shadow roots in upgradeCustomElements and handle View Transitions0d9be29 Two improvements to the custom element upgrade logic in the client router:
1. upgradeCustomElements now recursively walks into shadow roots. querySelectorAll('*') only searches light DOM — elements like
hoist scripts/styles to head + data-layout for router detection7bcebf0 Two fixes for client-side navigation with light DOM layouts:
1. SSR hoistHeadTags: leading <script> and <style> tags from the body are moved to <head>. Tailwind browser script, @theme styles, and
remove View Transitions from same-layout swap + cache-bust fetches1c5a06a Two fixes: - Remove startViewTransition from swapSlotContent to eliminate race conditions where custom elements aren't upgraded during the transition animation. Add a deferred microtask upgrade pass instead.
MutationObserver safety net for custom element upgrades2d260dc Add a global MutationObserver that watches for any custom element inserted into the document and calls customElements.upgrade() on it. This catches elements that the browser fails to auto-upgrade after DOM operations like replaceChildren — regardless of timing, View
load page component modules on same-layout navigationa04a974 Root cause of both bugs: on client-side navigation within the same layout, the router swapped <main> content but did NOT load the new page's component modules. Components like <new-post> appeared in the DOM as plain HTMLElement — never upgraded, no event handlers, no
use add-only head merge for same-layout navigation8e9948c mergeHead() was removing the Tailwind-generated <style> tag from <head> during same-layout navigation because it wasn't in the server-rendered HTML (it's generated at runtime by the Tailwind browser CDN).
forward streamed Suspense resolvers on same-layout nav09c06e3 Streamed Suspense templates (<template data-webjs-resolve="...">) are emitted at body level, AFTER the </div> that closes the data-layout wrapper. During same-layout navigation the router swaps only the <main> contents inside the wrapper, so those resolvers were dropped
skip non-HTML responses and extensions; document data-no-router9af54bf Three gaps in the same-origin interception logic that produced a broken/hung/blank page:
render-client compile() crashed on holes inside comments / raw-text2311969 The client-side compile() function destructures only strings from the template result, but two branches (state==='comment' and state==='rawtext') referenced an undefined values binding. Any template with an interpolation inside an HTML comment or <script>/<style> body would throw
share component registry across module instancesf3757aa When the cli is installed globally (or hoisted at a different level than the user's app), Node resolves @webjskit/core from each importer's location and may load TWO instances of the package — one for @webjskit/server (server-side) and one for the user's app
server
v0.2.1
1 change
Fixes
share component registry across module instancesf3757aa When the cli is installed globally (or hoisted at a different level than the user's app), Node resolves @webjskit/core from each importer's location and may load TWO instances of the package — one for @webjskit/server (server-side) and one for the user's app
cli
v0.2.1
1 change
Fixes
share component registry across module instancesf3757aa When the cli is installed globally (or hoisted at a different level than the user's app), Node resolves @webjskit/core from each importer's location and may load TWO instances of the package — one for @webjskit/server (server-side) and one for the user's app
server
v0.2.0
1 change
Features
use esbuild for TS on both SSR + hydration; default to no buildeb0b0ee The dev server now registers an esbuild ESM loader hook (module.register()) at startup. Every server-side .ts import flows through esbuild's transform — same transformer the dev server already used for browser-bound .ts requests. SSR and hydration produce
cli
v0.2.0
1 change
Features
use esbuild for TS on both SSR + hydration; default to no buildeb0b0ee The dev server now registers an esbuild ESM loader hook (module.register()) at startup. Every server-side .ts import flows through esbuild's transform — same transformer the dev server already used for browser-bound .ts requests. SSR and hydration produce
server
v0.1.1
25 changes
Features
auto-vendor npm deps, module graph, lazy loading266d041 Auto-vendor: scan client imports for bare specifiers, bundle via esbuild, serve at /__webjs/vendor/<pkg>.js, auto-populate import map.
Module graph: build dependency graph at startup, emit transitive
abstract wire serializer, decouple from superjson81ab2eb New Serializer interface with getSerializer/setSerializer. superjson is the default but can be swapped via webjs.config.js. Decouples the RPC layer from a single dependency.
optional catch-all, nested not-found, loading→Suspense, metadata routes332f41f - [[...slug]] optional catch-all matches with and without params - not-found.ts collected at every segment level (nearest wins) - loading.ts auto-wraps page in Suspense boundary with loading as fallback - Metadata routes: sitemap.ts, robots.ts, manifest.ts, icon.ts,
webjs test, webjs check, webjs create with AI guardrails6e85e5d - webjs test: discovers and runs unit + E2E tests - webjs check: validates app against 6 convention rules - webjs create: scaffolds opinionated project with CONVENTIONS.md, cross-agent configs (.cursorrules, .windsurfrules, copilot-instructions),
batteries included — sessions, jobs, pub/sub, storage, cache6292558 Convention over configuration, like Rails: - Set REDIS_URL → cache, sessions, rate limiter, pub/sub, and jobs all use Redis automatically. Zero config. - Set S3_BUCKET → file storage uses S3. Otherwise local disk.
NextJs-style cache + NextAuth-style auth + WebSocket broadcast19418b3 Removed: storage (s3/disk), background jobs — not core framework.
auto-wrap layout output with data-layout for client router306c512 The SSR pipeline now automatically wraps the outermost layout's rendered output in <div data-layout="LAYOUT_ID">. Users no longer need to add data-layout manually — the framework handles it.
SSR pages default to no-store, caching is opt-in5a4468c SSR responses now send Cache-Control: no-store by default. Pages are dynamic — the developer opts in to caching explicitly via metadata.cacheControl (e.g. 'public, max-age=60'). This ensures client-side navigation always gets fresh content without needing
Class.register() — drop the import.meta.url argument283c2b5 Authors no longer write Counter.register(import.meta.url); the call is just Counter.register(). Module URLs (used by SSR to emit <link rel=modulepreload> hints) are derived server-side at boot by scanning the app tree for `class X extends WebComponent { static tag =
adopt standard customElements.define — drop static tag + .register()bb61186 The web-standard registration convention (same shape Lit uses for its non-decorator form):
class Counter extends WebComponent { ... }
Counter.register('my-counter') as the idiomatic registrationa0f1f68 Adds a static register(tag) method on WebComponent. Delegates to the internal registry (which in turn calls customElements.define / the server shim), so every registration still lands in the native customElements map and webjs's mirror map. Authors write:
rename core package webjs → @webjs/core for npm publish49e2d6b The unscoped webjs name is already taken on npm (webjs@0.6.1, unrelated project). Moving to the @webjs scope lets us publish @webjs/core, @webjs/server, and @webjs/cli together.
rescope @webjs → @webjskit (org name 'webjs' unavailable on npm)316e2be The @webjs organization is not available on npm (likely reserved because the unscoped webjs package is held by an unrelated project). Switching to the @webjskit scope, which we own.
social-preview cards for website, docs, and blogb16fc68 Adds proper Open Graph + Twitter summary_large_image metadata so the three apps preview cleanly when shared on WhatsApp, Twitter/X, LinkedIn, Slack, Discord, etc.
Fixes
decode percent-encoded URL paths before filesystem lookups72caa14 Root cause of ALL client-side interactivity failing in production:
The SSR shell emits module imports with literal filesystem paths: import "/app/blog/[slug]/page.ts"
bundle superjson into a single ESM file for the browsera13cbea Root cause of ALL client-side JS failing:
superjson internally imports \copy-anything\ which imports \is-what\ — both as bare specifiers. The browser's import map only had an entry for
production hardening — cache eviction + CSP nonce supportc559de2 Cache eviction: TS transform cache capped at 500 entries, vendor bundle cache at 100. FIFO eviction prevents unbounded memory growth in long-running servers.
remove all auto-detection magic from session and cache3a52624 Cleaned up REDIS_URL auto-detection references from session.js and cache.js JSDoc. Session defaults to cookie (explicit, stateless). Cache defaults to memory (explicit). User switches to Redis by calling setStore/passing storeSession — no env var magic.
hoist scripts/styles to head + data-layout for router detection7bcebf0 Two fixes for client-side navigation with light DOM layouts:
1. SSR hoistHeadTags: leading <script> and <style> tags from the body are moved to <head>. Tailwind browser script, @theme styles, and
exclude server-only modules from <link rel=modulepreload>36ea61b Server-only files (either .server.{js,ts,mjs,mts} or plain .ts with a 'use server' directive) were appearing in modulepreload links because the transitive-deps walk treats them the same as any other import. Browsers fetched them on every page load — the response body is a
hard guardrail — never serve server-file source to the clienta4df002 The dev HTTP layer used to look up .server.* / 'use server' files via the action index built at boot; on index miss it fell through to tsResponse() and returned the RAW SOURCE. That's a serious leak vector: DB credentials, scrypt routines, privileged business logic
resolve esbuild from app dir, not server dir4987a04 When @webjskit/cli is installed globally (the typical case for end-users), import('esbuild') from packages/server/src/dev.js walks up from the global install tree — never reaching the user app's node_modules. The dev server then served .ts files as
cli
v0.1.4
1 change
Fixes
resolve esbuild from app dir, not server dir4987a04 When @webjskit/cli is installed globally (the typical case for end-users), import('esbuild') from packages/server/src/dev.js walks up from the global install tree — never reaching the user app's node_modules. The dev server then served .ts files as
cli
v0.1.2
1 change
Features
use icon-only theme toggle in scaffoldeb90718 Aligns the scaffolded <theme-toggle> with the website, docs, and blog versions — a circular icon button with sun/moon/system SVGs. The previous scaffold rendered the literal text 'Auto'/'Light'/'Dark' in a pill, which was inconsistent with the rest of the project.
cli
v0.1.1
27 changes
Features
webjs test, webjs check, webjs create with AI guardrails6e85e5d - webjs test: discovers and runs unit + E2E tests - webjs check: validates app against 6 convention rules - webjs create: scaffolds opinionated project with CONVENTIONS.md, cross-agent configs (.cursorrules, .windsurfrules, copilot-instructions),
default theme to system across website, docs, blog, and scaffolder7257b49 Theme toggle defaults to 'system' instead of 'light'. Bootstrap scripts no longer force data-theme — they only set it when the user has an explicit localStorage preference. CSS prefers-color-scheme handles the default. Updated all three apps + the webjs create scaffolder template.
migrate client tests to WTR + Playwright, real browser testing1ce3888 Added @web/test-runner + @web/test-runner-playwright + playwright. Client-side tests (renderer, directives, Shadow DOM) now run in real Chromium — no more linkedom/jsdom fake DOM. Server tests stay on node:test (they need Node APIs).
add Playwright MCP server for browser debugging4c5746e Added Microsoft's @playwright/mcp as both user-level (global) and project-level MCP server. End users get it via webjs create (.claude.json).
Playwright MCP gives AI agents direct browser control: navigate, click,
git pre-commit hook blocks commits on main/masterca31525 Added .git/hooks/pre-commit locally and .hooks/pre-commit in the scaffolder template. webjs create now runs git init + configures core.hooksPath to .hooks/ so the hook is tracked in the repo and works for everyone who clones it.
add .env.example for scaffolder and blog example9994261 Shows all convention-over-configuration env vars: PORT, SESSION_SECRET, REDIS_URL (auto-scales everything), S3_BUCKET (cloud storage), DATABASE_URL. Includes generation command for SESSION_SECRET.
NextJs-style cache + NextAuth-style auth + WebSocket broadcast19418b3 Removed: storage (s3/disk), background jobs — not core framework.
scaffold templates (full-stack + API) and code generatorsb8f569b webjs create <name> --template api Backend-only: route wrappers over typed server actions, no pages/SSR
SaaS template — webjs create --template saas1ab8ea0 Scaffolds a complete SaaS starter with auth, dashboard, Prisma: - app/login, app/signup — auth pages - app/dashboard with middleware auth guard + settings page - app/api/auth/[...path]/route.ts — auth API handlers
auto-scope ALL CSS selectors for light DOM componentsbdb3280 scopeCSS() now prefixes every CSS rule with the component tag name: input { color: red } → comments-thread input { color: red } :host { display: block } → comments-thread { display: block }
typed component props via defineComponent + .d.ts overlay9f0b09a The existing static-properties pattern carried no compile-time type information, so authors had to duplicate every field as declare x: T for editor intellisense. This adds a TypeScript overlay plus a tiny runtime helper so the descriptor map is the single source of truth for
Class.register() — drop the import.meta.url argument283c2b5 Authors no longer write Counter.register(import.meta.url); the call is just Counter.register(). Module URLs (used by SSR to emit <link rel=modulepreload> hints) are derived server-side at boot by scanning the app tree for `class X extends WebComponent { static tag =
adopt standard customElements.define — drop static tag + .register()bb61186 The web-standard registration convention (same shape Lit uses for its non-decorator form):
class Counter extends WebComponent { ... }
Counter.register('my-counter') as the idiomatic registrationa0f1f68 Adds a static register(tag) method on WebComponent. Delegates to the internal registry (which in turn calls customElements.define / the server shim), so every registration still lands in the native customElements map and webjs's mirror map. Authors write:
rename core package webjs → @webjs/core for npm publish49e2d6b The unscoped webjs name is already taken on npm (webjs@0.6.1, unrelated project). Moving to the @webjs scope lets us publish @webjs/core, @webjs/server, and @webjs/cli together.
rescope @webjs → @webjskit (org name 'webjs' unavailable on npm)316e2be The @webjs organization is not available on npm (likely reserved because the unscoped webjs package is held by an unrelated project). Switching to the @webjskit scope, which we own.
favicon for all three apps + Prisma/SQLite in every scaffolde6ba242Favicon - scripts/generate-favicon.mjs renders the header-logo artwork (accent-orange linear gradient, rounded square, 1px inner highlight) at 512×512 via puppeteer and writes favicon.svg + favicon.png into
Fixes
dev mode uses node --watch for reliable file change detection792cde0 Root cause: Node's ESM module cache has no public invalidation API. When loadModule() cache-busts a page with ?t=timestamp, only the top-level module is re-imported. Transitive imports (server actions, queries, components, utils) resolve to the SAME cached URL and return
enforce commit-often and bypass-mode in all end-user templates016d39f - guard-main-merge.sh template now respects bypass mode - CLAUDE.md template: commit-often is step 1 of the workflow - CONVENTIONS.md template: commits listed first in required checklist - .cursorrules, .windsurfrules, copilot-instructions: COMMIT OFTEN
enforce commit-AND-push in all agent instructions286ce7f Commit without push is half the job. Updated CLAUDE.md, all templates (CLAUDE.md, CONVENTIONS.md, .cursorrules, .windsurfrules, copilot- instructions.md) to say 'commit AND push' everywhere.
branch-context hook must ALWAYS fire, even in bypass modec7a35b5 The bypass-mode early-exit was defeating the entire purpose of the branch guard. Editing on main is a safety issue — the hook should always prompt, regardless of mode. Merge/push guard can respect bypass (those are approval prompts), but branch check is a safety net.
merging ALWAYS requires user permission, even in bypass modea74450e Removed all 'auto-merge' language from AGENTS.md, CLAUDE.md, and every agent config template (.cursorrules, .windsurfrules, copilot-instructions, CONVENTIONS.md). Merge guard hook no longer respects bypass mode — it always prompts. Merging is irreversible and must never be autonomous.
all hooks respect bypass mode — no prompts in autonomous mode24dd724 Both guard-main-merge.sh and guard-branch-context.sh (local + templates) now respect skipDangerousModePermissionPrompt. In bypass mode: no prompts for edits, merges, or pushes. The agent follows best practices via AGENTS.md instructions, not hook interruptions.
clean permission model — feature branches are free, only merge asks9aa740e Hooks: on feature branches, commit/push are fully free (no prompts). Only merging into main prompts for approval. Bypass mode allows everything including merges.
update all end-user templates for WTR + Playwright test stack06b77c7 Updated CONVENTIONS.md (browser test section, test matrix table), .cursorrules, .windsurfrules, copilot-instructions.md to reference WTR + Playwright instead of Puppeteer/E2E. Removed stale WEBJS_E2E references.
revert blog components with bare selectors to shadow DOMa36400d Components with bare element selectors (input, button, ul, h2, a) MUST use shadow DOM — bare selectors would leak to the whole page in light DOM. Reverted: comments-thread, auth-forms, chat-box, new-post, error-card.
remove auto-scope CSS hack, clean shadow/light DOM conventionf91ceb0 Shadow DOM = scoped styles (static styles = css, adoptedStyleSheets). Light DOM = global styles (Tailwind, global CSS, no static styles). Pick one. Don't fake shadow DOM scoping with regex.