Skip to content

Releases: lessjs-run/lessjs

0.21.5

24 May 04:29

Choose a tag to compare

LessJS v0.21.5 — Hardening Line Complete

2026-05-24 · 16 packages · 794 tests · 11/11 SOP gates green

v0.21.5 is the final release of the v0.21.x hardening line. After v0.21.0 proved
Reactive DSD, this line hardened the core API, DSD conformance, adapter
architecture, and Hub trust pipeline. v0.22.0 Edge Full-Stack is now unblocked.


What Changed

Core API Tightening

RenderError now carries stable code and severity for downstream gate
consumption. DsdComponent.render() is formally TemplateResult-first.
The full public API surface is classified into 4 tiers and documented.

SOP-001 ·
SOP-002 ·
SOP-003 ·
API Reference

DSD Conformance Proven

New conformance tests verify shadowrootmode, delegatesFocus, slotAssignment,
and host/template structure against WHATWG standards. DSD gate: 112 known
Shoelace SSR diagnostics, 0 unknown errors.

SOP-004 ·
SOP-005 ·
SOP-006

Adapter Architecture Cleaned

Route tagName is now resolved at build time via static scanning — no more
module.tagName || 'fallback' runtime probes in generated SSR code.
Eliminates IMPORT_IS_UNDEFINED build warnings.

SOP-007 ·
SOP-008

Hub Trust Gate Hardened

manifestHash now required as 64-char lowercase hex. Submission artifacts
validated for non-empty path/contentType/content. hub-submit hashes real
custom-elements-manifest content.

SOP-009

v0.22 Entry Gate Passed

All 8 SOP-010 entry questions cleared. Core API classified. Stale contracts
removed. DSD conformance tested. Build warnings accepted with finite
thresholds. Hub validation deterministic. v0.22 scope confirmed narrow.

SOP-010 ·
STATUS


CI & Tooling

Tool Purpose
sop-gate.yml 12-job CI pipeline — all 11 SOP gates + SSG starter proof
codeql.yml Weekly TypeScript security scan (security-extended,security-and-quality)
3 Copilot Agents ADR Reviewer / SOP Tracker / Test Guardian in .github/agents/

Benchmark (Windows, Deno 2.x)

Metric Value
Build ~7s, 439 pages, 655 KB total JS
Test suite 794 passed, ~21s
E2E 92 passed, ~3.5min
DSD diagnostics 112 known, 0 unknown

Full Benchmark


All Packages

@lessjs/core          0.21.5
@lessjs/signals        0.21.5
@lessjs/rpc            0.21.5
@lessjs/ui             0.21.5
@lessjs/adapter-lit    0.21.5
@lessjs/adapter-vanilla 0.21.5
@lessjs/adapter-react  0.21.5
@lessjs/adapter-vite   0.21.5
@lessjs/app            0.21.5
@lessjs/content        0.21.5
@lessjs/i18n           0.21.5
@lessjs/create         0.21.5
@lessjs/hub            0.21.5
@lessjs/cem            0.21.5
@lessjs/compat-check   0.21.5
@lessjs/style-sheet    0.21.5

Verification

deno task fmt:check     # 463 files ✅
deno task lint          # 278 files ✅
deno task typecheck     # 34 entry points ✅
deno audit              # clean ✅
deno task test          # 794 passed ✅
deno task build         # exit 0 ✅
deno task dsd:check-report  # gate passed ✅
deno task hub:validate --strict --json  # 2/2 ✅
deno task hub:check-index    # up to date ✅
deno task docs:check-strategy  # 5 checks ✅
deno task test:e2e      # 92 passed ✅

Known Signals

  • Chunk-size budget warnings (4 chunks >50KB). Client budget test is green.
    Tracked for v0.22 code-split.
  • DSD: 112 Shoelace SSR boundary diagnostics, all known, 0 unknown.
    Threshold will tighten in v0.22.
  • E2E requires --workers=1 on Windows (Playwright worker teardown noise).

What's Next

v0.22.0 Edge Full-Stack — ISR cache adapter boundary, Cloudflare Workers KV,
Deno KV, edge HTTP handler, deployment guides.

0.21.0

24 May 00:58

Choose a tag to compare

Reactive DSD — Zero-framework reactivity for Web Components

v0.21.0 makes DsdElement strong enough for interactive Web Components without Lit, React, JSX, a compiler, or a virtual DOM.

One import. One template syntax. One event model.

import { DsdElement, html, signal } from '@lessjs/core';

class LessCounter extends DsdElement {
  #count = signal(0);

  render() {
    return html`
      <button @click=${() => this.#count.value++}>
        Count: ${this.#count}
      </button>
    `;
  }
}

What's New

🎯 Reactive DSD Runtime

DsdElement.render() now returns html tagged template literals. Signal writes trigger microtask-batched component-local rerenders via fine-grained DOM patching (_patchBindings). Static render(): string components keep existing behavior — zero breaking change for non-reactive components.

🔒 Safe Templates by Default

Every dynamic interpolation is escaped or handled by a typed binding rule. Text escaping, attribute escaping, URL protocol sanitization — all built in. unsafeHTML() is the explicit, runtime-verifiable trust boundary.

Binding Syntax Behavior
Text ${value} HTML-escaped
Attribute class="${sig}" Attribute-escaped
URL href="${url}" Protocol-sanitized
Boolean ?disabled="${sig}" Toggles attribute
Property .value="${sig}" Runtime-only, no serialization
Event @click="${fn}" Runtime-only, no serialization
Raw HTML ${unsafeHTML(html)} Trust boundary

📡 Streaming DSD

renderDSDStream() returns ReadableStream<Uint8Array> — plug directly into new Response(stream) in Deno, Cloudflare Workers, or browser Web Streams. Shell first, components in order, footer last. Failed components emit bare-tag fallback; the stream continues. Built for v0.22 ISR handlers.

🔗 Unified Event Model

static hydrateEvents removed from DsdElement. Every component — static or interactive — uses html + @click. No more dual code paths. No more double-fire risk. One mental model.

📦 Core Package Split — 16 Packages

Three new standalone packages extracted from @lessjs/core:

Package Purpose
@lessjs/compat-check SSR compatibility classifier (4-tier system)
@lessjs/cem Custom Elements Manifest parser/reader
@lessjs/style-sheet Cross-environment CSSStyleSheet shim

All 16 packages at v0.21.0 with independent deno.json, publish configs, and subpath exports.

🔌 ReactiveHost Protocol

Explicit subscribeTo() / requestReactiveUpdate() contract replaces Duck Typing. External signal libraries target ReactiveHost, not internal DsdElement methods. DsdElement declares implements ReactiveHost.

📊 Test Coverage Expansion

Test file Before After
reactive-dsd.test.ts 1 13
template.test.ts 7 15
streaming-dsd.test.ts 7 (new)

Breaking Changes

  • static hydrateEvents removed. Migrate to html + @click:
    // Before
    static hydrateEvents = [{ selector: 'button', event: 'click', method: '_click' }];
    render(): string { return '<button>Click</button>'; }
    
    // After
    render() { return html`<button @click=${this._click}>Click</button>`; }
  • renderDSDStream() returns ReadableStream<Uint8Array>, not a string stream. Use onChunk for text diagnostics.
  • HydrateEventDescriptor type deprecated. Removed from core runtime; still exported for adapter backward compat. v1.0 removal.
  • batch() deprecated. DsdElement already microtask-batches. Emits console.warn. v1.0 removal.

Architecture Integrity

This release adds no new dependencies to @lessjs/core:

  • ❌ No DOM diff engine
  • ❌ No virtual DOM
  • ❌ No JSX transform
  • ❌ No compiler
  • ❌ No Vite / Hono / Node / npm runtime dependency

Complex UI remains an Island responsibility. Reactive DSD is for small DSD-native interactions — counters, toggles, search, theme switches.


Code Quality

Cross-reviewed by architecture audit (8.2→8.8/10):

  • -950 lines duplicate code eliminated (compat-check/cem)
  • ./types subpath export added to @lessjs/core
  • DANGEROUS_KEYS extracted to independent security.ts
  • connectedCallback simplified via _hydrateOrRender() extraction
  • Dead functions (_now, _resolveStreamPart) removed

Verification

deno fmt          488 files ✅
deno lint         276 files, 0 problems ✅
deno typecheck    clean ✅
deno test         787 passed, 0 failed ✅
deno build        clean ✅
deno test:e2e     92 passed ✅

Known Limitations

  • No DOM diffing. Signal re-patching covers text bindings; complex subtrees go to Islands. Intentional — adding diff would create a second rendering engine.
  • computed() signal re-subscription timing. The inner effect chain in @lessjs/signals needs hardening for reliable computed→DOM updates. Tracked for v0.22.
  • Third-party WC Tier 1 (DSD pre-rendering) remains fragile. Each new WC library may need snapshot fixes. Tier 2 (tag output + browser upgrade) works universally.

Documentation


Full Changelog: https://github.com/lessjs-run/lessjs/blob/dev/docs/changelog/v0.21.0.md

Next: v0.22.0 — Edge Full-Stack (ISR handler, KV adapters, deployment guides)

v0.20.0

22 May 09:43

Choose a tag to compare

Ocean-Island Architecture: decouple 9 DSD components from Lit and rebuild on a
zero-dependency DsdElement (native HTMLElement) base class. Lit is retained
only for less-hero-ping (Island component). Bundle size reduced by ~50%
(12KB → 6KB gzip). All CSS moved from Lit css\`toCSSStyleSheet, and custom--less-*` design tokens replaced with Open Props.

Phase 0: Infrastructure (SOP-001 → SOP-003)

DsdElement — Zero-Dependency Base Class (SOP-001)

  • NEW: packages/core/src/dsd-element.tsDsdElement extends HTMLElement
    with render(): string, static styles: CSSStyleSheet, static hydrateEvents,
    static observedAttributes, static delegatesFocus, static formAssociated.
    Ports createRenderRoot() DSD detection and _hydrateEvents() from
    WithDsdHydration mixin. ~155 lines.
  • NEW: packages/core/__tests__/dsd-element.test.ts — 8 test cases covering
    DSD detection, CSR fallback, event hydration, AbortController cleanup,
    M-17 guard, CSSStyleSheet merging, and delegatesFocus.

SSR CSSStyleSheet Extraction (SOP-002)

  • EDIT: packages/core/src/render-dsd.ts — Native CSSStyleSheet extraction
    inserted before adapter loop (+15 lines). Components with static styles: CSSStyleSheet have their CSS rules serialized directly into DSD <style>
    tags without requiring an adapter.

Open Props Token Migration (SOP-003)

  • NEW: packages/ui/src/open-props-tokens.ts — Pure CSSStyleSheet with
    inline Open Props values (spacing, gray palette, brand colors, radii,
    shadows, typography, easing). Zero CDN dependency.
  • DELETE: packages/ui/src/tokens/color-values.ts, packages/ui/src/tokens/colors.ts
  • EDIT: packages/ui/deno.json — Removed lit and @lessjs/adapter-lit
    imports from the UI package.
  • EDIT: packages/adapter-lit/src/index.ts — Marked DsdLitElement and
    WithDsdHydration as @deprecated.

Phase 1: Display + Attribute-Driven Components (SOP-004, SOP-005)

All 5 components migrated from DsdLitElement to DsdElement:

Component Lines Type CSS Parts
less-card ~96 Pure display container, body
less-callout ~80 Pure display container, icon, content
less-step-card ~100 Pure display container, indicator, title, content
less-button ~251 Attribute-driven control
less-input ~254 Attribute-driven wrapper, label, control, error

Key changes per component:

  • render() returns string instead of TemplateResult
  • css\`new CSSStyleSheet()+replaceSync()`
  • --less-* CSS variables → Open Props (--gray-*, --size-*, --brand, etc.)
  • @propertystatic observedAttributes + attributeChangedCallback
  • @click / @inputstatic hydrateEvents (declarative event binding)
  • _dsdHydrated → nothing hack removed (DsdElement handles DSD automatically)
  • All user-facing elements now expose part="..." for ::part() external styling
  • Component registration guarded: if (!customElements.get(tagName)) customElements.define(...)

Phase 2: Interactive Components (SOP-006, SOP-007)

less-theme-toggle (SOP-006)

  • localStorage persistence + sun/moon icon swap
  • static hydrateEvents: single click handler on toggle button
  • CSS Parts: toggle, icon-sun, icon-moon

less-code-block (SOP-006)

  • Prism syntax highlighting injected into shadow root via _tryHighlight()
  • Clipboard API with :state(copied) CSS custom state feedback
  • Copy button DOM updates via _updateCopyButtonDOM()
  • CSS Parts: copy, body

less-dialog (SOP-006)

  • Native <dialog> element with showModal()/close()
  • Focus management via delegatesFocus
  • Inert sibling elements for accessibility (_syncInert())
  • ESC + overlay click to close
  • CSS Parts: overlay, dialog, header, close, body, footer

less-layout (SOP-007)

  • Largest component (1202 lines) — 3-step layered migration
  • SPA navigation via Navigation API (navigate() + _loadContent() fetch-and-swap)
  • Event delegation at shadow root for nav clicks (data-nav attribute)
  • Mobile responsive: sidebar slide-in, hamburger menu, bottom tab bar
  • less-theme-toggle imported for SSR recursive DSD rendering
  • CSS Parts: container, header, sidebar, main, footer, nav, nav-toggle

Phase 3: Islands + CSS Parts + Search (SOP-008, SOP-009, SOP-010)

less-search (SOP-008)

  • Migrated from DsdLitElement to DsdElement
  • Overlay styles moved from inline style.cssText to CSSStyleSheet
    injected into document.adoptedStyleSheets
  • Overlay DOM created imperatively in document.body
  • FlexSearch integration preserved (dynamic import + index loading)
  • Cmd+K global keyboard shortcut
  • SPA-safe: state reset on connectedCallback()
  • CSS Parts: trigger, icon, label, shortcut

less-hero-ping (SOP-009 — Island, kept Lit)

  • Retained Lit — only Island component needing framework reactivity
  • Added part="dot-static" and part="dot-animated" for CSS Parts
  • Lit @click event binding preserved
  • This establishes the Island boundary: Lit for reactive-only, DSD for everything else

CSS Parts Universal Coverage (SOP-010)

  • All 10 components now expose part="..." attributes on key elements
  • JSDoc @csspart documentation added to each component file
  • ::part() consumer customization supported across the entire library

Phase 4: Build Verification & Regression Testing (SOP-011, SOP-012)

Build Verification

  • Zero Lit imports in 9 DSD components (verified via grep)
  • All 10 components import from @lessjs/core
  • packages/ui/deno.json: lit and @lessjs/adapter-lit removed
  • Manifest updated: superclassNameDsdElement, less.adaptervanilla
  • Target: bundle ≤ 6KB gzip (≥ 50% reduction from v0.19)

Regression Testing

  • Visual regression: all component variants render identically
  • Interactive regression: click, input, toggle, copy, dialog open/close
  • SSR output: <style> tags populated, <slot> elements present,
    no @click, .prop, ?attr Lit syntax in DSD output
  • Edge cases: disabled JS, rapid open/close, empty slots

Breaking Changes

  • Custom CSS variables renamed: --less-* → Open Props equivalent
    (e.g., --less-size-4--size-4, --less-text-primary--gray-9).
    Consumers using --less-* custom properties directly will need to update.
  • @lessjs/adapter-lit deprecated for DSD components.
    Only less-hero-ping (Island) still uses it. New DSD components must
    extend DsdElement from @lessjs/core.
  • DsdLitElement deprecated. Use DsdElement for new DSD components.
  • lessDesignTokens removed from @lessjs/ui. Use
    openPropsTokenSheet from @lessjs/ui instead.

Migration Guide

For component consumers (no changes needed)

<!-- No change — the same HTML works -->
<less-button variant="primary">Click me</less-button>

For component authors (migrating from Lit to DsdElement)

// v0.19 (Lit)
import { css, html } from 'lit';
import { DsdLitElement } from '@lessjs/adapter-lit';
class MyButton extends DsdLitElement {
  static styles = css`
    .btn {
      color: var(--less-accent);
    }
  `;
  render() {
    return html`
      <button>...</button>
    `;
  }
}

// v0.20 (DsdElement)
import { DsdElement } from '@lessjs/core';
const s = new CSSStyleSheet();
s.replaceSync(`.btn { color: var(--brand); }`);
class MyButton extends DsdElement {
  static styles = s;
  render() {
    return `<button part="control">...</button>`;
  }
}

Post-Release Audit Fixes (2026-05-21)

P0 Fix

  • openPropsTokenSheet missing :host wrapper — CSS variables were bare
    declarations without a selector, making them invalid for both
    CSSStyleSheet.replaceSync() and SSR <style> output. Wrapped in :host {}.

P1 Fixes

  • DsdElement: DSD path missing adoptedStyleSheets_applyStyles() extracted
    as shared method, DSD hydration now also applies adoptedStyleSheets to prevent
    style loss after innerHTML re-render.
  • DsdElement: _hydrateEvents() memory leak — Previous AbortController not
    aborted before creating new one. Fixed by aborting on re-entry.
  • less-button: _reRender() replaceChildren bugreplaceChildren moved
    light DOM children into shadow root, breaking slot projection. Removed manual
    DOM manipulation.
  • 4 components: _esc() SSR-unsafedocument.createElement('div') fails
    in SSR. Replaced with pure string regex in less-callout, less-step-card,
    less-dialog, less-input.
  • less-search: CSSStyleSheet SSR crash — Module-level new CSSStyleSheet()
    converted to lazy getOverlaySheet() initialization.
  • less-search: missing openPropsTokenSheet — Added to static styles array.
  • less-code-block, less-dialog: incorrect formAssociated — Neither is a form
    element; removed static override formAssociated = true.
  • less-input: hardcoded error color#e55var(--error, #dc3545).
  • Homepage: missing search buttonDocsHome.render() didn't include
    <less-search slot="header-actions">. Non-homepage pages work via
    _renderer.ts injection, but homepage bypasses it. Added import and slot.

Token System Cleanup

  • Deleted: design-tokens.ts + tokens/ directory (animation, effects,
    radius, spacing, typography) — superseded by openPropsTokenSheet.
  • Clean naming: All --less-* CSS variables replaced with semantic names:
    --text-primary, --bg-surface, --border, --brand, etc.
  • www pages:...
Read more

0.19.0

20 May 16:07

Choose a tag to compare

v0.19.0 — Registry Hub + Component Browser

Release Date: 2026-05-17
Branch: dev → main
SOP: docs/sop/v0.19.0-platform-hub.md, docs/sop/v0.19.0-component-browser.md

Summary

The first public-facing Registry Hub with package search, compatibility evidence,
CLI-driven submission pipeline, component detail pages with rendered previews, and
the less add CLI. This is the most significant feature release since v0.18.0.

Phase 1: Hub MVP

New Package: @lessjs/hub

  • src/schema.ts — HubPackageRecord, HubIndex, HubSubmission types + validators
  • src/builder.ts — Build Hub records from validation/build artifacts
  • src/indexer.ts — Build lightweight search indices
  • src/snapshot.ts — Snapshot management (encode/decode)
  • src/submitter.ts — Submission bundler + GitHub PR logic
  • src/scanner.ts — Scan node_modules for WC packages, generate records with CEM data
  • src/snapshot-renderer.ts — SSR + Happy DOM snapshot rendering
  • src/cli/hub-submit.tsless hub submit CLI
  • src/cli/validate.tsdeno task hub:validate record validator
  • src/cli/check-index.tsdeno task hub:check-index drift detector
  • src/cli/less-add.tsless add <package> CLI
  • src/cli/render-happy.ts — Happy DOM subprocess renderer

Hub CI

  • .github/workflows/hub-ci.yml — Validate submissions, check scoped paths,
    conditional auto-merge

Registry UI (www)

  • /registry/ — Package search + filter + sort
  • /registry/:package — Package detail (compatibility, install guidance, components)
  • /registry/:package/:component — Component detail (rendered preview, API reference,
    usage snippet, install command)

Fixture Data

  • Shoelace (39 components, client-only)
  • @lessjs/ui (8 components, SSR-capable)
  • Media Chrome (6 components, client-only)

Phase 2: Component Browser

less add <package> CLI

  • Auto-detect JSR/npm/local source
  • Component count from hub-index or CEM
  • --apply flag updates deno.json imports
  • --verbose shows details

Component Detail Pages

  • Per-component SSG pages with getStaticPaths()
  • Breadcrumb navigation (Registry → Package → Component)
  • Rendered preview (SSR or Happy DOM snapshot)
  • Usage code snippet
  • Compatibility badge
  • API Reference from CEM (Attributes, Events, Slots tables)
  • Install guidance (less add <package>)
  • Related components in same package

Snapshot Rendering

  • renderSnapshotLit() for SSR-capable Lit components (@lessjs/ui)
  • renderSnapshotWithHappyDom() via subprocess for npm packages (Shoelace, Media Chrome)
  • Demo attributes for visible previews (sl-alert: open, sl-button: variant=primary, etc.)
  • CEM data extraction: attributes, events, slots from custom-elements.json
  • manifestHash computed from CEM content (SHA-256)
  • adoptedStyleSheets serialization: Lit's constructable stylesheets are now
    converted to inline <style> tags before snapshot serialization. Fixes blank
    previews for sl-card, sl-button, and all other Shoelace components that
    rely on adoptedStyleSheets for shadow DOM styling.

Audit Remediation (P0/P1/P2)

P0 Fixes

  • Hub validator scoped traversal: validate.ts now correctly enters @scope/
    directories; validates all 3 records instead of 1
  • Hub index deterministic comparison: check-index.ts ignores updatedAt when
    comparing; no false drift detection
  • manifestHash integrity: scanner.ts now passes CEM content to builder;
    Shoelace has 64-char SHA-256 hash; empty hash flagged as warning

P1 Fixes

  • Snapshot XSS sanitizer: sanitizeSnapshot() strips <script>, <iframe>,
    on* event handlers, javascript: URLs before unsafeHTML rendering
  • happy-dom removed from @lessjs/core barrel: renderWithDomSimulation moved to
    @lessjs/core/dom-simulation subpath export only; core barrel is zero-npm again
  • CI/publish completeness: test.yml adds test-hub job; publish.yml adds
    @lessjs/hub; hub-ci.yml scoped path glob fixed (**/*.json)

P2 Fixes

  • WC name validation: island.ts now enforces WHATWG custom element name rules
    (lowercase, hyphens, no reserved prefixes); schema.ts validates tag names in
    Hub records
  • typecheck coverage: Hub CLI files (validate.ts, check-index.ts, less-add.ts,
    scanner.ts) added to deno task typecheck

Verification

Gate Result
deno fmt --check ✅ 0 errors
deno lint ✅ 0 errors
deno task typecheck ✅ (includes hub CLI)
deno task test ✅ 729/729
deno task build
deno task hub:validate --strict ✅ 3 records valid
deno task hub:check-index ✅ index up to date

Phase 2+3 Completion (2026-05-18)

Phase 2: Component Browser — Final Items

  • Enhanced package list: SSR-capable vs client-only component breakdown per package
    (e.g. "8 SSR · 39 client"), "New" badge for packages submitted within 7 days
  • HubIndexEntry.submittedAt: Added to schema, indexer, and generated data for
    "New" badge calculation
  • snapshot-playwright.test.ts: Sanitizer, fixture HTML generation, and slot map
    construction unit tests

Phase 3: Snapshot v2 Playwright — Completion

  • snapshot-playwright.test.ts: 10 tests covering sanitizer (script/iframe/onclick/
    javascript: URL removal), fixture HTML generation, slot map construction with
    whitespace fix verification, and placeholder rendering

Post-Release Audit Remediation (2026-05-18)

SOP: docs/sop/audit-remediation-20260518.md
Source: 3 audit reports — 33 issues across 6 severity categories
Commits: d933851 cb570c7 a7bcf40 1ca497c ad2f49d bf64c99

Phase A: Fact Corrections (STATUS.md + ROADMAP.md)

  • Test count: STATUS.md updated from 715 → 729 (verified)
  • renderDSD() claim: Added "architecturally" qualifier + "Current implementation: SSG only"
  • Full-Stack Framework completion: 60% → 45% with weighted capability methodology table
  • Hub completion: 65% → 55%
  • Branch Status: "Phase 2 active" → "Phase 1/2/3 complete"
  • ROADMAP hub:scan count: 52/53 → 47/48 (actual)
  • Version Ladder: v0.19.0 duplicate row merged
  • Known Issues: Added dsd-report 72 errors, deno fmt panic, hub exports gap,
    check-index write issue

Phase B: Git Hygiene

  • Missing git tags created: v0.17.5 (ed88eaa), v0.18.0 (0322699),
    v0.18.3 (1d3c003)
  • v0.19.0 tag: Created after workspace cleanup
  • 77 uncommitted changes: Committed as grouped commit
  • ADR-0033: Added to git tracking

Phase C: Code/Product Fixes

  • Hub JSR exports: Added ./cli/less-add, ./cli/validate, ./cli/check-index
    to packages/hub/deno.json
  • hub:check-index split: Read-only check-index (exits 1 on drift, no write) +
    new hub:index:update task (explicit write with --allow-write)
  • dsd-report-gate: New packages/hub/src/cli/dsd-report-gate.ts — reads
    dsd-report.json, classifies errors by recoverability, checks against threshold
    (currently Infinity/report-only; ≤10 in v0.20, 0 in v0.21)
  • Snapshot placeholder honesty: snapshot-playwright.ts — two success: true
    success: false for Playwright-not-available and render-failure fallbacks
  • Route scanner TODO: Added TODO comment for v0.21 regex→AST migration

Phase D: CI/Infrastructure

  • test.yml expanded: test-hub job adds hub:validate + hub:check-index steps;
    new hub-scan job with Playwright Chromium; new dsd-report-gate job
  • ADR-0034: New ADR — hermetic hub snapshots (3-phase esm.sh → local migration)
  • E2E retries: Local retries 0→1 as Windows-specific mitigation

Phase E: Narrative & Positioning Corrections

  • @lessjs/ui DSD-native: All "DSD-native" claims qualified with "planned v0.21+"
  • README over-marketing: Replaced "other frameworks cannot match" with accurate
    differentiation statement
  • Full-Stack Framework maturity: Added early-stage maturity warning to README
  • renderDSD() description: Qualified as "architecturally" timing-agnostic in README
  • WWW navigation decision: Created docs/conversation/www-navigation-decision.md
    recording 5-section decision (/framework/ + /hub/ + /engine/ + /ui/ + /blog/)
  • Hub early access: Registry index page added "Early Access" badge + 3-package note
    with submit link

Phase F: Strategic Roadmap Adjustments

  • ISR promoted to P0: Rationale — SSG-only "full-stack framework" is a contradiction
  • Vue adapter → P2/Deferred: Requires Hydration + ISR + Lit/Vanilla validation first
  • Supabase → P3/Deferred: Full-stack groundwork first; generic request context first
  • Hub Ecosystem Building → P1: After Hydration ships; target 10+ packages by Phase 6 end
  • @lessjs/ui DSD-native Evolution: Added to ROADMAP Phase 7 as explicit vision with
    prerequisites

Audit Remediation Verification

Gate Result
deno task test ✅ 729/729
deno task typecheck
git status --short ✅ clean workspace
git tag -l ✅ all version tags

Breaking Changes

  • @lessjs/core barrel no longer exports renderWithDomSimulation,
    buildDomSimulationReport, DomSimulationOptions, DomSimulationResult.
    Import from @lessjs/core/dom-simulation instead.
  • hub:check-index no longer writes files on drift — use hub:index:update
    for explicit write access

Phase 6: SSG Resilient Rendering + Visual Overhaul (2026-05-...

Read more

0.18.3

17 May 04:12

Choose a tag to compare

Changelog: v0.18.3

Release date: 2026-05-17
Release type: experimental patch (opt-in only)

Overview

v0.18.3 adds an experimental DOM simulation path for browser-dependent Web
Components. Using Happy DOM as the underlying DOM environment, selected
client-only components can attempt server-side rendering through an isolated,
timeout-bound pipeline.

This is the last v0.18.x release. The v0.18 series delivers the Universal
WC Engine: CEM parser → compatibility classifier → validation CLI → safe
install flow → DOM simulation experiment.

What's New

DOM Simulation Renderer (@lessjs/core/dom-simulation)

New module packages/core/src/dom-simulation.ts with:

  • renderWithDomSimulation() — renders a Web Component through Happy DOM
  • buildDomSimulationReport() — builds the dsd-report.json section

Config (domSimulation)

lessjs({
  ssr: {
    domSimulation: 'off' | 'explicit',
    domSimulationTimeoutMs: 500,
  },
});

Default: 'off'. Must be explicitly enabled.

Design

Rule Implementation
Disabled by default Config default is 'off'
Timeout-bound Configurable timeout (default 500ms)
Isolated Fresh Window per render attempt
Degrades safely Failure → client-only fallback
Reported Results in dsd-report.json

Happy DOM Integration

Chosen over self-implementation and JSDOM per ADR-0029. Happy DOM provides
customElements.define(), connectedCallback(), and Lit lifecycle support
out of the box, with an active maintenance community.

New Public API

Types

  • DomSimulationReport — report section in dsd-report.json
  • DomSimulationAttempt — per-component attempt detail
  • DomSimulationResult — single render result
  • DomSimulationOptions — render options (tag name, source code, timeout)

Config

  • FrameworkOptions.ssr.domSimulation'off' | 'explicit'
  • FrameworkOptions.ssr.domSimulationTimeoutMs — timeout in ms

Functions

  • renderWithDomSimulation(options) — render through Happy DOM
  • buildDomSimulationReport(results) — build report section

ADRs

  • ADR-0029: Happy DOM for v0.18.3 DOM Simulation (Proposed)

Verification

  • default build does not use DOM simulation
  • explicit fixture can render through experimental path
  • timeout fixture fails predictably
  • unsupported API fixture falls back or fails with clear reason
  • dsd-report.json records strategy and result
  • no package is silently upgraded from client-only to SSR
  • deno task fmt && deno task lint && deno task typecheck
  • deno task test (681 tests, 8 new)
  • deno task build

Breaking Changes

None. v0.18.3 adds optional config and optional API surface.

  • New optional config: ssr.domSimulation, ssr.domSimulationTimeoutMs
  • New optional exports: renderWithDomSimulation, buildDomSimulationReport
  • New optional subpath export: @lessjs/core/dom-simulation
  • No behavior changes to existing code

Migration Guide

For users: No action needed. v0.18.3 is a drop-in replacement for v0.18.2.

To experiment with DOM simulation:

lessjs({
  ssr: {
    domSimulation: 'explicit',
    domSimulationTimeoutMs: 1000,
  },
});

End of v0.18.x Series

v0.18.3 is the final v0.18.x release. The series delivered:

Version Capability Status
v0.18.0 CEM parser + 4-tier classifier + auto-detection
v0.18.1 validate-manifest CLI
v0.18.2 less add safe install flow
v0.18.3 DOM simulation experiment (Happy DOM)

Next up: v0.19.x — Platform + Registry Hub.

Diff Summary

git diff --stat v0.18.2..v0.18.3

packages/core/__tests__/dom-simulation.test.ts (new, 119 lines)
packages/core/deno.json (1 line changed)
packages/core/src/dom-simulation.ts (new, 236 lines)
packages/core/src/index.ts (13 lines changed)
packages/core/src/types.ts (40 lines changed)

Stats:

  • 2 new files
  • 3 modified files
  • 8 new tests (681 total)
  • +409 / -0 lines

0.18.2

17 May 04:12

Choose a tag to compare

Changelog: v0.18.2

Release date: 2026-05-17
Release type: minor patch (CLI + project mutation)

Overview

v0.18.2 adds less add — a safe install flow for adding third-party Web
Component packages to LessJS projects. Every install is preceded by
validation (v0.18.1), so invalid packages are rejected before any files
are touched.

What's New

Safe Install Flow (@lessjs/core/less-add)

New module packages/core/src/less-add.ts with plan-based install logic:

Flow: Resolve → Validate → Plan → Review → Apply

Dry Run First

less add @scope/package --dry-run

Prints the full plan without changing any files:

  • Package source and version
  • Compatibility tier
  • Tags to register
  • File mutations (add/modify/remove)
  • Warnings and rejected tags

Validation Gate

Before any file mutation, the v0.18.1 validator is run. If the manifest
fails validation, the add stops immediately with zero file changes.

File Mutations Generated

For a valid SSR-capable package, the plan includes:

File Change
vite.config.ts Add to packageIslands array
vite.config.ts Add to ssr.noExternal list (SSR-capable only)
src/less-imports.ts Create/update client registration

Compatibility-Aware Behavior

Tier Behavior
ssr-capable Full setup: packageIslands + noExternal + registration
client-only packageIslands + registration only (no noExternal)
rejected No mutations — validation error stops the flow

Rollback Safety

If any write fails, the plan prints:

  • Which files were changed
  • Recovery command or manual rollback instructions
  • Does not continue to build

New Public API

Added to @lessjs/core:

Types:

  • AddPlan — full install plan with tags, mutations, status
  • AddTagEntry — per-tag entry in the install plan
  • FileMutation — single file change descriptor
  • PackageSource — local / jsr / npm

Functions:

  • generateAddPlan(options) — generate an install plan for a package

Exports:

  • @lessjs/core/less-add — install flow module
  • @lessjs/core/cli/less-add — CLI entry point

Verification

  • less add ./fixtures/ssr-capable --dry-run changes no files
  • less add ./fixtures/client-only --dry-run reports client-only outcome
  • invalid package fails before file writes
  • real install updates expected files only
  • rerunning install is idempotent (same input → same plan)
  • uninstall/manual rollback instructions are printed when needed
  • deno task fmt && deno task lint && deno task typecheck
  • deno task test (673 tests, 14 new)
  • deno task build

Breaking Changes

None. v0.18.2 adds new API surface without modifying existing interfaces.

  • New optional exports: generateAddPlan
  • New optional subpath exports: @lessjs/core/less-add, @lessjs/core/cli/less-add
  • No behavior changes to existing code

Migration Guide

For users: No action needed. v0.18.2 is a drop-in replacement for v0.18.1.

To add a package:

# First, check what would happen
deno run -A jsr:@lessjs/core/cli/less-add @scope/package --dry-run

# Then apply
deno run -A jsr:@lessjs/core/cli/less-add @scope/package

Next Steps

With v0.18.2 complete, the project can now proceed to:

  • v0.18.3: DOM simulation experiment for client-only components
  • See docs/sop/v0.18.3-dom-simulation-experiment.md

Diff Summary

git diff --stat v0.18.1..v0.18.2

packages/core/__tests__/less-add.test.ts (new, 193 lines)
packages/core/deno.json (2 lines changed)
packages/core/src/cli/less-add.ts (new, 123 lines)
packages/core/src/index.ts (10 lines changed)
packages/core/src/less-add.ts (new, 253 lines)

Stats:

  • 3 new files
  • 2 modified files
  • 14 new tests (673 total)
  • +581 / -0 lines

0.18.1

17 May 02:51

Choose a tag to compare

Changelog: v0.18.1

Release date: 2026-05-17
Release type: minor patch (CLI + diagnostics)

Overview

v0.18.1 adds less validate-manifest — a deterministic validation command for
CEM manifests. Users and CI pipelines can now verify a third-party Web Component
package's compatibility before installing or rendering it.

What's New

Core Validation Module (@lessjs/core/validate-manifest)

New module packages/core/src/validate-manifest.ts with comprehensive validation:

Check Code Severity What it validates
Schema version MISSING_SCHEMA_VERSION error schemaVersion field
Modules array MISSING_MODULES error modules array exists
Empty modules EMPTY_MODULES warning modules array is not empty
Module path MISSING_MODULE_PATH warning Each module has a path field
Tag name EMPTY_TAG_NAME error Tag name is not empty
Tag name format INVALID_TAG_NAME error Tag name contains a hyphen
Duplicate tags DUPLICATE_TAG error No duplicate tag across modules
Absolute path INVALID_MODULE_PATH error Module paths are relative, not absolute
Path traversal INVALID_MODULE_PATH error No ../ outside package root
URL path INVALID_MODULE_PATH error No HTTP/HTTPS paths
Superclass path INVALID_SUPERCLASS_PATH error Superclass module paths are valid
less.ssr type INVALID_SSR_VALUE error Must be boolean
less.dsd type INVALID_DSD_VALUE error Must be boolean
Hydrate strategy INVALID_HYDRATE_STRATEGY error Must be one of: eager/lazy/idle/visible
Layer INVALID_LAYER error Must be one of: dsd-static/dsd-interactive/pure-island
No custom elements NO_CUSTOM_ELEMENTS warning At least one custom-element declaration
Exports no decls EXPORTS_WITHOUT_DECLARATIONS warning Module has exports but no declarations

CLI (less validate-manifest)

Three modes:

# Human-readable default
deno run -A jsr:@lessjs/core/cli/validate-manifest ./custom-elements.json

# Machine-readable JSON
deno run -A jsr:@lessjs/core/cli/validate-manifest ./custom-elements.json --json

# Strict mode (fails on warnings too)
deno run -A jsr:@lessjs/core/cli/validate-manifest ./custom-elements.json --strict

Exit codes:

  • 0 — manifest is valid
  • 1 — manifest has errors (or warnings in strict mode)

New Public API

Added to @lessjs/core:

Types:

  • ValidationDiagnostic — structured error/warning with code, severity, message, fix
  • ValidatedTag — per-tag validation result with compatibility tier
  • ManifestValidationReport — full validation report

Functions:

  • validateManifest(manifest) — validate a parsed CustomElementsManifest
  • validateManifestFromJson(json) — convenience wrapper for raw JSON

Exports:

  • @lessjs/core/validate-manifest — validation module
  • @lessjs/core/cli/validate-manifest — CLI entry point

Verification

  • valid Less manifest returns exit code 0
  • CEM-only package is valid but client-only
  • duplicate tag returns exit code 1
  • path traversal returns exit code 1
  • malformed Less extension returns actionable error
  • JSON output is deterministic
  • deno task fmt && deno task lint && deno task typecheck
  • deno task test (659 tests, 29 new)
  • deno task build

Breaking Changes

None. v0.18.1 adds new API surface without modifying existing interfaces.

  • New optional exports: validateManifest, validateManifestFromJson
  • New optional subpath exports: @lessjs/core/validate-manifest, @lessjs/core/cli/validate-manifest
  • No behavior changes to existing code

Migration Guide

For users: No action needed. v0.18.1 is a drop-in replacement for v0.18.0.

For CI/CD pipelines: Add less validate-manifest to your pre-install gates:

deno run -A jsr:@lessjs/core/cli/validate-manifest ./custom-elements.json --json

For package authors: Run validate-manifest on your custom-elements.json to
catch issues before publishing:

deno run -A jsr:@lessjs/core/cli/validate-manifest ./custom-elements.json

Next Steps

With v0.18.1 complete, the project can now proceed to:

  • v0.18.2: less add — one-click install + configuration flow
  • See docs/sop/v0.18.2-less-add-install-flow.md

Diff Summary

git diff --stat v0.18.0..v0.18.1

packages/core/__tests__/validate-manifest.test.ts (new, 513 lines)
packages/core/deno.json (2 lines changed)
packages/core/src/cli/validate-manifest.ts (new, 104 lines)
packages/core/src/index.ts (9 lines changed)
packages/core/src/types.ts (72 lines changed)
packages/core/src/validate-manifest.ts (new, 466 lines)

Stats:

  • 3 new files
  • 3 modified files
  • 29 new tests (659 total)
  • +1,167 / -1 lines

0.18.0

17 May 02:47

Choose a tag to compare

Changelog: v0.18.0

Release date: 2026-05-17
Release type: minor (new capability, backward compatible)

Overview

v0.18.0 implements the Universal WC Engine Admission Layer, enabling LessJS to safely accept third-party Web Component packages through a validated compatibility model. Previously, LessJS could not distinguish between SSR-capable and client-only third-party packages, leading to potential runtime failures.

Complete SOP Steps

Step 1: CEM Parser ✅

Added packages/core/src/cem-parser.ts — parses standard custom-elements.json without executing package code:

  • Extracts tag names, module paths, exports, attributes, events, slots, CSS parts, and CSS custom properties
  • Preserves unknown CEM fields for future compatibility
  • Rejects invalid tag names and unresolved module paths
  • Detects duplicate tags before SSR registration
  • 19 tests passing

Step 2: Compatibility Classifier ✅

Added packages/core/src/compatibility.ts — 4-tier classification engine:

Tier Meaning Build behavior
ssr-capable Explicit Less manifest or adapter says SSR supported Import/register in SSR bundle
client-only Browser-only package or no SSR declaration Exclude from SSR bundle
rejected Invalid manifest, duplicate tag, unsafe path Fail before code generation
experimental-dom Opt-in DOM simulation candidate Render only when explicit flag enabled

Conservative defaults: CEM without Less extension → client-only (not SSR). 29 tests passing.

Step 3: SSR/client-only Planner ✅

Updated packages/adapter-vite/src/entry-descriptor.ts:

  • buildSsrAdmissionPlan() now accepts CompatibilityClassification[]
  • CEM classifications take precedence over island metadata
  • SsrAdmissionPlan.cemClassifications field added
  • 18 tests passing (9 new + 9 existing)

Step 4: Report Schema Extension ✅

  • packages/core/src/types.tsCemCompatibilityReport interface added to DsdBuildReport
  • packages/adapter-vite/src/cli/ssg-render.tsbuildCemCompatibilityReport() builder
  • packages/adapter-vite/src/build-context.tsPhase1Meta.cemClassifications field
  • Report schema version bumped to 1.1.0
  • cemCompatibility section in dsd-report.json

Step 5: Fixtures and Tests ✅

  • 4 new tests for CEM compatibility in ssg-report.test.ts
  • 6 new tests for CEM auto-detection in route-scanner.test.ts
  • Total: 630 tests passing

CEM Plugin Integration ✅

Completed the "last mile" integration that was missing from the original SOP:

  • packages/adapter-vite/src/route-scanner.tsscanCemManifests() + detectAndClassifyCemPackages()
    • Scans node_modules for custom-elements.json without executing package code
    • Handles scoped packages (@org/pkg), skips invalid JSON (non-fatal)
    • Calls parseCem() + classifyCemManifest() pipeline
  • packages/adapter-vite/src/index.tsbuildStart() now calls detectAndClassifyCemPackages()
    • Stores results in ctx.phase1.cemClassifications
    • Passes cemClassifications to buildEntryDescriptor()
    • Failure is non-fatal (best-effort, debug log only)
  • packages/core/deno.json — added ./compatibility subpath export

Package Version Update

Updated all 12 packages from 0.17.5 to 0.18.0:

  1. @lessjs/core
  2. @lessjs/adapter-lit
  3. @lessjs/adapter-react
  4. @lessjs/adapter-vanilla
  5. @lessjs/adapter-vite
  6. @lessjs/app
  7. @lessjs/content
  8. @lessjs/create
  9. @lessjs/i18n
  10. @lessjs/rpc
  11. @lessjs/signals
  12. @lessjs/ui

Note: Inter-package dependencies use ^0.17.0 (caret range), so 0.18.0 is automatically compatible.

Verification

  • All 5 SOP items implemented
  • CEM plugin integration complete
  • 630 tests passing
  • deno task fmt:check && deno task lint && deno task typecheck
  • Committed and pushed to origin/dev

Breaking Changes

None. This is a minor release that adds:

  • New CEM parsing capability
  • New compatibility classification tiers
  • New report schema extensions
  • No API changes
  • No behavior changes (conservative defaults preserve existing behavior)

Migration Guide

For users: No action needed. v0.18.0 is a drop-in replacement for v0.17.5.

For third-party Web Component authors:

  • If your package has a custom-elements.json, it will now be auto-detected
  • By default, your components are treated as client-only (not SSR)
  • To enable SSR: add a LessJS manifest with ssr: true or use a supported adapter

For contributors:

  • When adding new compatibility tiers, update packages/core/src/compatibility.ts
  • When modifying CEM parsing, update packages/core/src/cem-parser.ts
  • Run deno task test to verify compatibility classification

Next Steps

With v0.18.0 complete, the project can now proceed to:

  • v0.18.1: less validate-manifest CLI built on top of the classifier
  • See docs/sop/v0.18.1-validate-manifest-cli.md (to be created)

Diff Summary

git diff --stat v0.17.5..v0.18.0

packages/core/src/cem-parser.ts (new)
packages/core/src/compatibility.ts (new)
packages/core/src/types.ts (CEM types added)
packages/adapter-vite/src/entry-descriptor.ts (cemClassifications support)
packages/adapter-vite/src/entry-renderer.ts (cemClassifications support)
packages/adapter-vite/src/route-scanner.ts (CEM auto-detection)
packages/adapter-vite/src/build-context.ts (Phase1Meta.cemClassifications)
packages/adapter-vite/src/cli/ssg-render.ts (cemCompatibility report)
packages/adapter-vite/__tests__/ssr-admission.test.ts (18 tests)
packages/adapter-vite/__tests__/route-scanner.test.ts (CEM tests)
packages/adapter-vite/__tests__/ssg-report.test.ts (CEM report tests)
packages/core/deno.json (version bump + ./compatibility export)
packages/adapter-lit/deno.json (version bump)
packages/adapter-react/deno.json (version bump)
packages/adapter-vanilla/deno.json (version bump)
packages/adapter-vite/deno.json (version bump)
packages/app/deno.json (version bump)
packages/content/deno.json (version bump)
packages/create/deno.json (version bump)
packages/i18n/deno.json (version bump)
packages/rpc/deno.json (version bump)
packages/signals/deno.json (version bump)
packages/ui/deno.json (version bump)

Stats:

  • 25+ files changed
  • 2 new core modules (cem-parser, compatibility)
  • 630 tests passing
  • 12 version bumps

Consumer-Level Explanation (Simple Analogies)

What was the problem?

Imagine you're building a house, and you can use materials from:

  • Local suppliers (your own components) — you know exactly how they work
  • Third-party suppliers (npm packages) — you don't know if they work in SSR or only in browser

Previously, LessJS treated all third-party Web Components as potentially SSR-safe, which could cause runtime failures if a component tried to access browser APIs during server-side rendering.

What did we do in v0.18.0?

  1. Added a "material inspector" (CEM Parser)

    • Reads the "spec sheet" (custom-elements.json) for each third-party package
    • Understands what the component is and what it needs
  2. Added a "classification system" (Compatibility Classifier)

    • SSR-capable: safe for server rendering
    • Client-only: must run in browser only
    • Rejected: broken or unsafe
    • Experimental: needs opt-in
  3. Added "automatic detection" (CEM Plugin Integration)

    • Scans your node_modules for Web Component packages
    • Classifies them automatically
    • No configuration needed
  4. Added "reporting" (Report Schema Extension)

    • Every decision is logged in dsd-report.json
    • You can see exactly why each component was classified

Why should you care?

  • If you're a user: Your builds are now safer. Third-party components that don't support SSR are automatically excluded from the SSR bundle, preventing runtime failures.

  • If you're a library author: Add a custom-elements.json to your package. By default, it's treated as client-only (safe). Add LessJS metadata to enable SSR if your component supports it.

Analogy Summary

Before v0.18.0: "I hope all these third-party materials work on the server."

After v0.18.0: "Here's exactly which materials are safe for server, which need browser, and why. No more guessing."

0.17.5

17 May 02:20

Choose a tag to compare

Changelog: v0.17.5

Release date: 2026-05-17
Release type: patch (SOP compliance + test infrastructure)

Overview

This release completes the v0.17.4 SOP (Security Operational Procedure) by
adding the missing Step 1 (SSR import discovery audit) and Step 6 (test fixtures).

Complete SOP Steps

Step 1: Audit Current Discovery Paths ✅

Added SSR import discovery audit comments to 5 key files:

File Audit Content
packages/adapter-vite/src/entry-descriptor.ts Records how SsrAdmissionPlan handles 3 sources (local/package/nested)
packages/adapter-vite/src/entry-renderer.ts Records which islands become SSR imports (only renderableTags)
packages/adapter-vite/src/route-scanner.ts Records how islands are discovered (static scan, no import)
packages/core/src/render-dsd.ts Records how nested custom elements are detected and skipped
packages/core/src/types.ts Notes that this file only defines types (no import logic)

Why this matters: Previously, the SSR import logic was correct but undocumented.
If future developers modified the code, they might accidentally break the
SSR/admission boundary. The audit comments make the discovery paths explicit.

Step 6: Add Fixtures ✅

Created packages/adapter-vite/__tests__/fixtures/ with 4 test fixtures:

Fixture File Purpose
local-island-ssr-false.ts Tests that less.ssr = false local islands are placed into clientOnlyTags
package-manifest-ssr-false.ts Tests that package manifest declarations with ssr: false are treated as client-only
parent-with-client-child.ts Tests that parent components rendering client-only child tags are handled correctly
browser-only-component.ts Tests that browser-only components fail cleanly (no stack trace) when imported in SSR

Why this matters: Previously, we relied on the entire docs site for testing (slow, complex, not精准).
Now we have focused, minimal test cases that can verify boundary conditions quickly and independently.

New Test File: ssr-admission.test.ts

Created packages/adapter-vite/__tests__/ssr-admission.test.ts with 9 test cases:

  1. Local island with ssr=falseclientOnlyTags
  2. Package island with ssr=falseclientOnlyTags
  3. Local island with ssr=truerenderableTags
  4. Package island with ssr=truerenderableTags
  5. Duplicate tag → rejectedTags
  6. Parent with client-child → parent renderable, child client-only ✅
  7. Mixed islands → correct categorization ✅
  8. Plan records reasons for all tags ✅
  9. Decisions array has correct structure ✅

Coverage: These tests verify the buildSsrAdmissionPlan() function directly,
ensuring that the SSR/admission logic is correct at the unit test level.

Package Version Update

Updated all 11 packages from 0.17.4 to 0.17.5:

  1. @lessjs/core
  2. @lessjs/adapter-lit
  3. @lessjs/adapter-react
  4. @lessjs/adapter-vanilla
  5. @lessjs/adapter-vite
  6. @lessjs/app
  7. @lessjs/content
  8. @lessjs/create
  9. @lessjs/i18n
  10. @lessjs/rpc
  11. @lessjs/signals
  12. @lessjs/ui

Note: Inter-package dependencies use ^0.17.0 (caret range), so 0.17.5 is
automatically compatible. No dependency updates needed.

Verification

  • All 11 deno.json files updated to 0.17.5
  • 4 fixture files created in __tests__/fixtures/
  • 9 test cases in ssr-admission.test.ts
  • Audit comments added to 5 key files
  • deno task fmt:check && deno task lint && deno task typecheck
  • deno task test (new tests pass)
  • deno audit
  • Committed and pushed to origin/dev

Breaking Changes

None. This is a patch release that adds:

  • Documentation (audit comments)
  • Test infrastructure (fixtures + test cases)
  • No API changes
  • No behavior changes

Migration Guide

For users: No action needed. v0.17.5 is a drop-in replacement for v0.17.4.

For contributors:

  • When modifying SSR admission logic, update the audit comments in the 5 key files
  • Add new fixtures to __tests__/fixtures/ when testing boundary conditions
  • Run deno task test to verify SSR admission logic

Next Steps

With v0.17.5 complete, the project can now proceed to:

  • v0.18.0: Universal WC Engine (CEM parser + compatibility tiers)
  • See docs/sop/v0.18.0-universal-wc-engine.md (to be created)

Diff Summary

git diff --stat v0.17.4..v0.17.5

packages/adapter-vite/__tests__/fixtures/browser-only-component.ts (new)
packages/adapter-vite/__tests__/fixtures/local-island-ssr-false.ts (new)
packages/adapter-vite/__tests__/fixtures/package-manifest-ssr-false.ts (new)
packages/adapter-vite/__tests__/fixtures/parent-with-client-child.ts (new)
packages/adapter-vite/__tests__/ssr-admission.test.ts (new)
packages/adapter-vite/src/entry-descriptor.ts (audit comments)
packages/adapter-vite/src/entry-renderer.ts (audit comments)
packages/adapter-vite/src/route-scanner.ts (audit comments)
packages/core/src/render-dsd.ts (audit comments)
packages/core/src/types.ts (audit comments)
packages/adapter-lit/deno.json (version bump)
packages/adapter-react/deno.json (version bump)
packages/adapter-vanilla/deno.json (version bump)
packages/adapter-vite/deno.json (version bump)
packages/app/deno.json (version bump)
packages/content/deno.json (version bump)
packages/create/deno.json (version bump)
packages/i18n/deno.json (version bump)
packages/rpc/deno.json (version bump)
packages/signals/deno.json (version bump)
packages/ui/deno.json (version bump)

Stats:

  • 13 files changed
  • 4 new fixture files
  • 1 new test file (9 test cases)
  • 5 files with audit comments
  • 11 version bumps

Consumer-Level Explanation (Simple Analogies)

What was the problem?

Imagine you're building a house, and you have:

  • Local materials (local islands) - you know exactly what they are
  • Package materials (package islands) - from suppliers, need inspection
  • Nested components (child tags) - some need special handling

Previously, we had rules for how to handle each type, but the rules were undocumented.
If a new builder came in, they might not know why material X goes to location Y.

What did we do in v0.17.5?

  1. Added labels to everything (Step 1 audit)

    • Like putting sticky notes on every material: "This goes to SSR", "This stays client-side"
  2. Created sample materials for testing (Step 6 fixtures)

    • Like having "test materials" that you know will fail or succeed in certain conditions
    • Now you can quickly verify your rules work, without building a whole house
  3. Wrote tests using those samples (ssr-admission.test.ts)

    • Like running quality checks on your test materials
    • 9 specific checks that prove your rules work correctly

Why should you care?

  • If you're a user: You don't need to do anything. This is internal improvement.
  • If you're a contributor: Now it's much easier to:
    • Understand why SSR admission works the way it does
    • Add new package islands without breaking things
    • Verify your changes don't break the SSR/client-only boundary

Analogy Summary

Before v0.17.5: "I know these rules work, but I can't explain why. Hope I don't forget!"

After v0.17.5: "Here's exactly how SSR admission works, here are test cases proving it, and here's what to do if you add new island types."

0.17.4

17 May 02:19

Choose a tag to compare

v0.17.4 - Compatibility Boundary Hardening

Release type: patch / correctness and build robustness
Date: 2026-05-16

Summary

v0.17.4 turns the v0.17.3 compatibility boundary into enforceable build
behavior: client-only and browser-dependent Web Components no longer enter the
SSR path by accident, and one-command builds now exit cleanly. A post-tag fix
round resolved dev-mode SSR crashes caused by island modules evaluating in the
SSR runner before the DOM shim provides HTMLElement.

Changes

SSR Admission

  • Added an SsrAdmissionPlan to the Vite entry descriptor so SSR entry
    generation is driven by explicit renderable/client-only decisions.
  • Local islands can now expose static less metadata such as ssr: false,
    dsd, and hydrate without being imported during route discovery.
  • Package islands are treated conservatively before v0.18 validation: they are
    client-only unless future manifest validation explicitly admits them for SSR.
  • Nested custom-element rendering skips client-only tags instead of
    instantiating browser-only child components on the server.

Browser-Only Package Handling

  • Package manifest scanning now skips browser-only packages that fail with
    missing browser globals such as window, instead of failing the entire build.
  • The docs site can include Shoelace/Media Chrome style browser-first demos
    without producing repeated SSR stack traces.

Optional Adapter Resolution

  • Added optional stubs for @lessjs/adapter-lit,
    @lessjs/adapter-vanilla, and @lessjs/adapter-react in both the primary
    Vite plugin and the SSG sub-build path.
  • create-less generated projects can build through the one-command pipeline
    even when optional adapters are not installed locally.

Build Exit And Budgets

  • The one-command build CLI now exits explicitly after successful Vite
    completion, fixing the post-build hang observed in v0.17.3 work.
  • The docs build-output test now separates core client budget from showcase
    demo budget so demo UI libraries do not hide core bundle regressions.

Dev-Mode SSR Crash Fixes

These fixes address runtime errors that appeared after the initial v0.17.4 tag
when running deno task dev:

  • SSR-safe base class pattern: WithDsdHydration(globalThis.HTMLElement)
    crashes in the SSR module runner because HTMLElement is undefined at
    module evaluation time (the DOM shim has not loaded yet). Fixed in
    react-showcase.ts and media-chrome-showcase.ts with a conditional:
    typeof globalThis.HTMLElement !== 'undefined' ? WithDsdHydration(...) : class {}.
    When HTMLElement is unavailable, the island falls back to a plain class so
    module evaluation succeeds; browser behavior is unaffected since HTMLElement
    is always available there.

  • ADR 0014 patch now applies in dev + SSG: The customElements.define()
    idempotent patch in entry-renderer.ts was guarded by if (desc.isSSG),
    so dev-mode SSR got duplicate-define crashes. Removed the guard — the patch
    now applies unconditionally because island modules call customElements.define()
    as a side-effect in both modes, and the SSR dom-shim is not idempotent.

  • media-chrome dynamic import: Changed import 'media-chrome' (static,
    evaluates browser-only code at module load) to a dynamic import('media-chrome')
    inside connectedCallback(), guarded by typeof globalThis.HTMLElement !== 'undefined'. Media Chrome registers custom elements that require DOM APIs at
    import time; deferring to connectedCallback ensures the import only runs in
    the browser.

  • Dev task working directory: deno task dev was running from the project
    root, but the route scanner uses process.cwd() for relative paths. Changed
    the task to cd www && deno run ... so process.cwd() matches the Vite root.
    Also added --allow-ffi --allow-sys flags required by Vite 8 rolldown native
    bindings.

  • api-consumer fetch timing: Deferred the status fetch to
    this.updateComplete.then() in connectedCallback(), avoiding Lit update
    races inside a parent DSD shadow root.

  • WithDsdHydration connectedCallback infinite recursion: The
    WithDsdHydration mixin in both adapter-vanilla and adapter-react used
    Object.getPrototypeOf(Object.getPrototypeOf(this)) to call the parent
    class's connectedCallback. When the subclass (e.g. ReactShowcase) does
    not override connectedCallback, the two-step prototype walk finds the
    mixin's own method, causing infinite recursion (Maximum call stack size exceeded). Fixed by using the closure-captured superClass variable
    directly: superClass.prototype.connectedCallback.call(this). This affects
    all components using WithDsdHydration(HTMLElement) as their base class.

Reporting And Tests

  • dsd-report.json now includes admission decisions from the SSR plan.
  • Added tests for local metadata scanning, admission decisions, client-only
    nested tags, optional package stubs, and build-output budget boundaries.

Files Changed

File Change
packages/adapter-vite/src/entry-descriptor.ts Added SsrAdmissionPlan
packages/adapter-vite/src/entry-renderer.ts SSR admission filter + ADR 0014 dev+SSG patch
packages/adapter-vite/src/route-scanner.ts Static less metadata scanning, browser-only skip
packages/adapter-vite/src/build-context.ts packageManifests + packageIslandDecls
packages/adapter-vite/src/index.ts Optional adapter stubs
packages/adapter-vite/src/cli/build.ts Clean exit after Vite completion
packages/adapter-vite/src/cli/build-ssg.ts Optional adapter stubs in SSG path
packages/adapter-vite/src/cli/ssg-render.ts Manifest decisions in dsd-report
packages/core/src/render-dsd.ts Client-only nested tag skip
packages/core/src/render-nested.ts Client-only nested tag skip
packages/core/src/types.ts ManifestDecision type
packages/core/src/index.ts Export ManifestDecision
www/app/islands/react-showcase.ts SSR-safe conditional base class
www/app/islands/media-chrome-showcase.ts SSR-safe base class + dynamic media-chrome import
www/app/islands/api-consumer.ts Deferred fetch after first render
www/app/islands/shoelace-showcase.ts ssr: false metadata
www/app/routes/index/index.ts Showcase sections
www/vite.config.ts Optional adapter resolution
www/__tests__/build-output.test.ts Budget threshold adjustment (core 350→600KB)
packages/adapter-vanilla/src/dsd-hydration.ts Fix connectedCallback/disconnectedCallback recursion
packages/adapter-react/src/dsd-hydration.ts Fix connectedCallback/disconnectedCallback recursion
deno.json Fixed dev/preview tasks (cd www + --allow-ffi)
packages/adapter-vite/__tests__/entry-renderer.test.ts SSR filtering + admission tests
packages/adapter-vite/__tests__/route-scanner.test.ts Static metadata scanning tests
packages/adapter-vite/__tests__/ssg-report.test.ts Manifest decisions tests
packages/adapter-vite/__tests__/index-plugin.test.ts Optional stubs tests
packages/core/__tests__/render-dsd.test.ts Client-only nested tag tests

Second-Round Findings (Post-Tag, 2026-05-16)

After the initial v0.17.4 tag, a deployment-time investigation into a reported
Failed to fetch SW error and header/footer style loss uncovered three
independent issues that were fixed in subsequent commits.

SW Cache Robustness

The Service Worker error sw.js:19 Uncaught (in promise) TypeError: Failed to fetch had two causes:

  • Navigate requests (HTML pages): The SW used bare fetch(e.request) with
    no catch handler. When the network was unreachable (e.g. an expired Cloudflare
    Pages preview URL such as aad0fe18.lessjs.pages.dev), the uncaught promise
    rejection produced the sw.js:19 TypeError.
  • Asset requests: The cacheFirst strategy used fetch() without a
    try/catch. Network failures on asset fetches also propagated as uncaught
    rejections.

Fix (packages/adapter-vite/src/cli/ssg-render.ts):

  1. Navigate requests now use networkFirst with an offline fallback (empty 408
    response) instead of bare fetch(e.request).
  2. Asset requests in cacheFirst wrap fetch() in try/catch, returning a 408
    response on network failure instead of throwing.
  3. Non-asset, non-navigate requests (e.g. opaque third-party requests) are
    passed through without SW interception.

Package Island SSR Admission

Root cause: buildSsrAdmissionPlan() in entry-descriptor.ts treated all
source === 'package' islands as ...

Read more