---
package: "@webjsdev/core"
version: 0.2.0
date: 2026-04-28T12:20:33+05:30
commit_count: 43
---
## Features

- **add essential directives (unsafeHTML, live) with renderer integration** [`3d8a253`](https://github.com/webjsdev/webjs/commit/3d8a253)
  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
- **full lifecycle, controllers, error boundaries, selective hydration** [`231bc4c`](https://github.com/webjsdev/webjs/commit/231bc4c)
  Add shouldUpdate, willUpdate, firstUpdated, updated, changedProperties
  tracking. Add ReactiveController pattern (addController/removeController).
  Add client-side error boundaries (renderError lifecycle hook). Add
  static hydrate='visible' for IntersectionObserver-based deferred
- **add Context Protocol and Task controller** [`d68b207`](https://github.com/webjsdev/webjs/commit/d68b207)
  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 loading** [`266d041`](https://github.com/webjsdev/webjs/commit/266d041)
  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 guardrails** [`6e85e5d`](https://github.com/webjsdev/webjs/commit/6e85e5d)
  - 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 DOM** [`cf155d7`](https://github.com/webjsdev/webjs/commit/cf155d7)
  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 styles** [`75f6d17`](https://github.com/webjsdev/webjs/commit/75f6d17)
  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 components** [`bdb3280`](https://github.com/webjsdev/webjs/commit/bdb3280)
  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 renderer** [`893c99f`](https://github.com/webjsdev/webjs/commit/893c99f)
  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-in** [`01b55c2`](https://github.com/webjsdev/webjs/commit/01b55c2)
  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 router** [`306c512`](https://github.com/webjsdev/webjs/commit/306c512)
  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-in** [`5a4468c`](https://github.com/webjsdev/webjs/commit/5a4468c)
  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 overlay** [`9f0b09a`](https://github.com/webjsdev/webjs/commit/9f0b09a)
  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 argument** [`283c2b5`](https://github.com/webjsdev/webjs/commit/283c2b5)
  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`](https://github.com/webjsdev/webjs/commit/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 registration** [`a0f1f68`](https://github.com/webjsdev/webjs/commit/a0f1f68)
  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 publish** [`49e2d6b`](https://github.com/webjsdev/webjs/commit/49e2d6b)
  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`](https://github.com/webjsdev/webjs/commit/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 values** [`c792b6e`](https://github.com/webjsdev/webjs/commit/c792b6e)
  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-renders** [`df2a952`](https://github.com/webjsdev/webjs/commit/df2a952)
  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 true** [`016d5b5`](https://github.com/webjsdev/webjs/commit/016d5b5)
  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 rubric** [`a026010`](https://github.com/webjsdev/webjs/commit/a026010)
  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 fallback** [`5f5553c`](https://github.com/webjsdev/webjs/commit/5f5553c)
  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 navigation** [`4f213d1`](https://github.com/webjsdev/webjs/commit/4f213d1)
  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 layout** [`5875ed6`](https://github.com/webjsdev/webjs/commit/5875ed6)
  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 DOM** [`d89e7bb`](https://github.com/webjsdev/webjs/commit/d89e7bb)
  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 router** [`991d8d0`](https://github.com/webjsdev/webjs/commit/991d8d0)
  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 race** [`08312a9`](https://github.com/webjsdev/webjs/commit/08312a9)
  - 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-documenting** [`47b5168`](https://github.com/webjsdev/webjs/commit/47b5168)
  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 convention** [`f91ceb0`](https://github.com/webjsdev/webjs/commit/f91ceb0)
  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`](https://github.com/webjsdev/webjs/commit/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 navigation** [`b357e56`](https://github.com/webjsdev/webjs/commit/b357e56)
  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 elements** [`b012c38`](https://github.com/webjsdev/webjs/commit/b012c38)
  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 Transitions** [`0d9be29`](https://github.com/webjsdev/webjs/commit/0d9be29)
  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 detection** [`7bcebf0`](https://github.com/webjsdev/webjs/commit/7bcebf0)
  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 fetches** [`1c5a06a`](https://github.com/webjsdev/webjs/commit/1c5a06a)
  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 upgrades** [`2d260dc`](https://github.com/webjsdev/webjs/commit/2d260dc)
  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 navigation** [`a04a974`](https://github.com/webjsdev/webjs/commit/a04a974)
  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 navigation** [`8e9948c`](https://github.com/webjsdev/webjs/commit/8e9948c)
  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 nav** [`09c06e3`](https://github.com/webjsdev/webjs/commit/09c06e3)
  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-router** [`9af54bf`](https://github.com/webjsdev/webjs/commit/9af54bf)
  Three gaps in the same-origin interception logic that produced a
  broken/hung/blank page:
  
  1. Non-HTML extensions — /foo.pdf, /data.zip, /feed.xml, /posts.json,
- **render-client compile() crashed on holes inside comments / raw-text** [`2311969`](https://github.com/webjsdev/webjs/commit/2311969)
  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 instances** [`f3757aa`](https://github.com/webjsdev/webjs/commit/f3757aa)
  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
