Releases: lessjs-run/lessjs
0.21.5
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.
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.
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.
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 |
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=1on 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
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 hydrateEventsremoved. Migrate tohtml+@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()returnsReadableStream<Uint8Array>, not a string stream. UseonChunkfor text diagnostics.HydrateEventDescriptortype deprecated. Removed from core runtime; still exported for adapter backward compat. v1.0 removal.batch()deprecated. DsdElement already microtask-batches. Emitsconsole.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)
./typessubpath export added to@lessjs/coreDANGEROUS_KEYSextracted to independentsecurity.tsconnectedCallbacksimplified 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/signalsneeds 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
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.ts—DsdElement extends HTMLElement
withrender(): string,static styles: CSSStyleSheet,static hydrateEvents,
static observedAttributes,static delegatesFocus,static formAssociated.
PortscreateRenderRoot()DSD detection and_hydrateEvents()from
WithDsdHydrationmixin. ~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— NativeCSSStyleSheetextraction
inserted before adapter loop (+15 lines). Components withstatic styles: CSSStyleSheethave 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— PureCSSStyleSheetwith
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— Removedlitand@lessjs/adapter-lit
imports from the UI package. - EDIT:
packages/adapter-lit/src/index.ts— MarkedDsdLitElementand
WithDsdHydrationas@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()returnsstringinstead ofTemplateResultcss\`→new CSSStyleSheet()+replaceSync()`--less-*CSS variables → Open Props (--gray-*,--size-*,--brand, etc.)@property→static observedAttributes+attributeChangedCallback@click/@input→static hydrateEvents(declarative event binding)_dsdHydrated → nothinghack 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 withshowModal()/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-navattribute) - Mobile responsive: sidebar slide-in, hamburger menu, bottom tab bar
less-theme-toggleimported 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
DsdLitElementtoDsdElement - Overlay styles moved from inline
style.cssTexttoCSSStyleSheet
injected intodocument.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"andpart="dot-animated"for CSS Parts - Lit
@clickevent 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
@csspartdocumentation 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:litand@lessjs/adapter-litremoved- Manifest updated:
superclassName→DsdElement,less.adapter→vanilla - 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,?attrLit 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-litdeprecated for DSD components.
Onlyless-hero-ping(Island) still uses it. New DSD components must
extendDsdElementfrom@lessjs/core.DsdLitElementdeprecated. UseDsdElementfor new DSD components.lessDesignTokensremoved from@lessjs/ui. Use
openPropsTokenSheetfrom@lessjs/uiinstead.
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
:hostwrapper — 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 bug —replaceChildrenmoved
light DOM children into shadow root, breaking slot projection. Removed manual
DOM manipulation. - 4 components:
_esc()SSR-unsafe —document.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 lazygetOverlaySheet()initialization. - less-search: missing openPropsTokenSheet — Added to
static stylesarray. - less-code-block, less-dialog: incorrect formAssociated — Neither is a form
element; removedstatic override formAssociated = true. - less-input: hardcoded error color —
#e55→var(--error, #dc3545). - Homepage: missing search button —
DocsHome.render()didn't include
<less-search slot="header-actions">. Non-homepage pages work via
_renderer.tsinjection, but homepage bypasses it. Added import and slot.
Token System Cleanup
- Deleted:
design-tokens.ts+tokens/directory (animation, effects,
radius, spacing, typography) — superseded byopenPropsTokenSheet. - Clean naming: All
--less-*CSS variables replaced with semantic names:
--text-primary,--bg-surface,--border,--brand, etc. - www pages:...
0.19.0
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 + validatorssrc/builder.ts— Build Hub records from validation/build artifactssrc/indexer.ts— Build lightweight search indicessrc/snapshot.ts— Snapshot management (encode/decode)src/submitter.ts— Submission bundler + GitHub PR logicsrc/scanner.ts— Scan node_modules for WC packages, generate records with CEM datasrc/snapshot-renderer.ts— SSR + Happy DOM snapshot renderingsrc/cli/hub-submit.ts—less hub submitCLIsrc/cli/validate.ts—deno task hub:validaterecord validatorsrc/cli/check-index.ts—deno task hub:check-indexdrift detectorsrc/cli/less-add.ts—less add <package>CLIsrc/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
--applyflag updates deno.json imports--verboseshows 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 forsl-card,sl-button, and all other Shoelace components that
rely onadoptedStyleSheetsfor shadow DOM styling.
Audit Remediation (P0/P1/P2)
P0 Fixes
- Hub validator scoped traversal:
validate.tsnow correctly enters@scope/
directories; validates all 3 records instead of 1 - Hub index deterministic comparison:
check-index.tsignoresupdatedAtwhen
comparing; no false drift detection - manifestHash integrity:
scanner.tsnow 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 beforeunsafeHTMLrendering - happy-dom removed from @lessjs/core barrel:
renderWithDomSimulationmoved to
@lessjs/core/dom-simulationsubpath export only; core barrel is zero-npm again - CI/publish completeness:
test.ymladdstest-hubjob;publish.ymladds
@lessjs/hub;hub-ci.ymlscoped path glob fixed (**/*.json)
P2 Fixes
- WC name validation:
island.tsnow enforces WHATWG custom element name rules
(lowercase, hyphens, no reserved prefixes);schema.tsvalidates tag names in
Hub records - typecheck coverage: Hub CLI files (validate.ts, check-index.ts, less-add.ts,
scanner.ts) added todeno 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 calculationsnapshot-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:d933851cb570c7a7bcf401ca497cad2f49dbf64c99
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
topackages/hub/deno.json - hub:check-index split: Read-only
check-index(exits 1 on drift, no write) +
newhub:index:updatetask (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— twosuccess: true
→success: falsefor 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-indexsteps;
newhub-scanjob with Playwright Chromium; newdsd-report-gatejob - 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/corebarrel no longer exportsrenderWithDomSimulation,
buildDomSimulationReport,DomSimulationOptions,DomSimulationResult.
Import from@lessjs/core/dom-simulationinstead.hub:check-indexno longer writes files on drift — usehub:index:update
for explicit write access
Phase 6: SSG Resilient Rendering + Visual Overhaul (2026-05-...
0.18.3
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 DOMbuildDomSimulationReport()— 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.jsonDomSimulationAttempt— per-component attempt detailDomSimulationResult— single render resultDomSimulationOptions— 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 DOMbuildDomSimulationReport(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.jsonrecords 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
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-runPrints 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, statusAddTagEntry— per-tag entry in the install planFileMutation— single file change descriptorPackageSource— 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-runchanges no files -
less add ./fixtures/client-only --dry-runreports 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/packageNext 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
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 --strictExit codes:
0— manifest is valid1— manifest has errors (or warnings in strict mode)
New Public API
Added to @lessjs/core:
Types:
ValidationDiagnostic— structured error/warning with code, severity, message, fixValidatedTag— per-tag validation result with compatibility tierManifestValidationReport— full validation report
Functions:
validateManifest(manifest)— validate a parsedCustomElementsManifestvalidateManifestFromJson(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 --jsonFor 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.jsonNext 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
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 acceptsCompatibilityClassification[]- CEM classifications take precedence over island metadata
SsrAdmissionPlan.cemClassificationsfield added- 18 tests passing (9 new + 9 existing)
Step 4: Report Schema Extension ✅
packages/core/src/types.ts—CemCompatibilityReportinterface added toDsdBuildReportpackages/adapter-vite/src/cli/ssg-render.ts—buildCemCompatibilityReport()builderpackages/adapter-vite/src/build-context.ts—Phase1Meta.cemClassificationsfield- Report schema version bumped to 1.1.0
cemCompatibilitysection indsd-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.ts—scanCemManifests()+detectAndClassifyCemPackages()- Scans
node_modulesforcustom-elements.jsonwithout executing package code - Handles scoped packages (
@org/pkg), skips invalid JSON (non-fatal) - Calls
parseCem()+classifyCemManifest()pipeline
- Scans
packages/adapter-vite/src/index.ts—buildStart()now callsdetectAndClassifyCemPackages()- Stores results in
ctx.phase1.cemClassifications - Passes
cemClassificationstobuildEntryDescriptor() - Failure is non-fatal (best-effort, debug log only)
- Stores results in
packages/core/deno.json— added./compatibilitysubpath export
Package Version Update
Updated all 12 packages from 0.17.5 to 0.18.0:
@lessjs/core✅@lessjs/adapter-lit✅@lessjs/adapter-react✅@lessjs/adapter-vanilla✅@lessjs/adapter-vite✅@lessjs/app✅@lessjs/content✅@lessjs/create✅@lessjs/i18n✅@lessjs/rpc✅@lessjs/signals✅@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: trueor 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 testto verify compatibility classification
Next Steps
With v0.18.0 complete, the project can now proceed to:
- v0.18.1:
less validate-manifestCLI 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?
-
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
- Reads the "spec sheet" (
-
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
-
Added "automatic detection" (CEM Plugin Integration)
- Scans your
node_modulesfor Web Component packages - Classifies them automatically
- No configuration needed
- Scans your
-
Added "reporting" (Report Schema Extension)
- Every decision is logged in
dsd-report.json - You can see exactly why each component was classified
- Every decision is logged in
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.jsonto 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
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:
- Local island with
ssr=false→clientOnlyTags✅ - Package island with
ssr=false→clientOnlyTags✅ - Local island with
ssr=true→renderableTags✅ - Package island with
ssr=true→renderableTags✅ - Duplicate tag →
rejectedTags✅ - Parent with client-child → parent renderable, child client-only ✅
- Mixed islands → correct categorization ✅
- Plan records reasons for all tags ✅
- 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:
@lessjs/core✅@lessjs/adapter-lit✅@lessjs/adapter-react✅@lessjs/adapter-vanilla✅@lessjs/adapter-vite✅@lessjs/app✅@lessjs/content✅@lessjs/create✅@lessjs/i18n✅@lessjs/rpc✅@lessjs/signals✅@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.jsonfiles updated to0.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 testto 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?
-
Added labels to everything (Step 1 audit)
- Like putting sticky notes on every material: "This goes to SSR", "This stays client-side"
-
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
-
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
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
SsrAdmissionPlanto the Vite entry descriptor so SSR entry
generation is driven by explicit renderable/client-only decisions. - Local islands can now expose static
lessmetadata such asssr: false,
dsd, andhydratewithout 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 aswindow, 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-reactin both the primary
Vite plugin and the SSG sub-build path. create-lessgenerated 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 becauseHTMLElementisundefinedat
module evaluation time (the DOM shim has not loaded yet). Fixed in
react-showcase.tsandmedia-chrome-showcase.tswith a conditional:
typeof globalThis.HTMLElement !== 'undefined' ? WithDsdHydration(...) : class {}.
WhenHTMLElementis unavailable, the island falls back to a plain class so
module evaluation succeeds; browser behavior is unaffected sinceHTMLElement
is always available there. -
ADR 0014 patch now applies in dev + SSG: The
customElements.define()
idempotent patch inentry-renderer.tswas guarded byif (desc.isSSG),
so dev-mode SSR got duplicate-define crashes. Removed the guard — the patch
now applies unconditionally because island modules callcustomElements.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 dynamicimport('media-chrome')
insideconnectedCallback(), guarded bytypeof globalThis.HTMLElement !== 'undefined'. Media Chrome registers custom elements that require DOM APIs at
import time; deferring toconnectedCallbackensures the import only runs in
the browser. -
Dev task working directory:
deno task devwas running from the project
root, but the route scanner usesprocess.cwd()for relative paths. Changed
the task tocd www && deno run ...soprocess.cwd()matches the Vite root.
Also added--allow-ffi --allow-sysflags required by Vite 8 rolldown native
bindings. -
api-consumer fetch timing: Deferred the status fetch to
this.updateComplete.then()inconnectedCallback(), avoiding Lit update
races inside a parent DSD shadow root. -
WithDsdHydration connectedCallback infinite recursion: The
WithDsdHydrationmixin in bothadapter-vanillaandadapter-reactused
Object.getPrototypeOf(Object.getPrototypeOf(this))to call the parent
class'sconnectedCallback. When the subclass (e.g.ReactShowcase) does
not overrideconnectedCallback, the two-step prototype walk finds the
mixin's own method, causing infinite recursion (Maximum call stack size exceeded). Fixed by using the closure-capturedsuperClassvariable
directly:superClass.prototype.connectedCallback.call(this). This affects
all components usingWithDsdHydration(HTMLElement)as their base class.
Reporting And Tests
dsd-report.jsonnow 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 asaad0fe18.lessjs.pages.dev), the uncaught promise
rejection produced thesw.js:19TypeError. - Asset requests: The
cacheFirststrategy usedfetch()without a
try/catch. Network failures on asset fetches also propagated as uncaught
rejections.
Fix (packages/adapter-vite/src/cli/ssg-render.ts):
- Navigate requests now use
networkFirstwith an offline fallback (empty 408
response) instead of barefetch(e.request). - Asset requests in
cacheFirstwrapfetch()in try/catch, returning a 408
response on network failure instead of throwing. - 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 ...