- Status: ACCEPTED
- Date: 2026-05-16
- Updated: 2026-05-16 (implementation status)
| Section | v0.15 Delivered | v0.16 Planned | Notes |
|---|---|---|---|
| §1 RendererProtocol | ✅ | — | RenderAdapter fully removed |
| §2 renderDSD() → RenderOutput | ❌ Types only | ✅ | Return type still Promise<string>; RenderOutput type defined + exported |
| §3 Module split | ✅ | — | 4 files as specified |
| §4 DSD report output | ❌ | ✅ | DsdRenderCollector works, getReport() works, but dsd-report.json not written by SSG |
| §5 Named adapters | ✅ | — | registerAdapter + getAdapter + getRegisteredAdapters |
| §6 create CLI | ✅ | — | JSR resolution + project name validation |
| §7 customElementRegistry | ✅ | — | Type simplified to boolean |
| RenderHooks | ❌ Types only | ✅ | Interface defined but not wired into pipeline |
| RenderInput | ✅ Types only | ✅ | Type defined + exported; not used as renderDSD() parameter |
ADR-0024 defined the strategic direction: LessJS is a Web Standards-first DSD/Web Components application framework. v0.15's goal is to productize the existing DSD renderer into a reusable rendering kernel.
The previous rendering interface (RenderAdapter) had three optional methods and
no error taxonomy:
interface RenderAdapter {
isTemplate?: (value: unknown) => boolean;
render?: (value: unknown, tagName: string) => Promise<string>;
extractStyles?: (componentClass: CustomElementConstructor) => string | undefined;
}Problems:
- No output contract —
renderDSD()returns a bare string. Errors, metrics, and hydration hints are lost or handled via side channels (collector mutation, console logs, HTML comments). - No error classification — instantiation failures, render crashes,
nested CE errors, and style extraction failures are all handled ad-hoc inside
a 360-line
renderDSD()function. - No lifecycle hooks — adapters cannot observe or intervene in the render pipeline (before render, after render, on error).
- Single adapter only —
adapter-registry.tsstores one module-level variable. Multiple adapters or adapter composition is impossible. - Metrics are afterthoughts —
DsdRenderCollectorexists but its output is never persisted or reported.
RenderAdapter is fully removed — no aliases, no backward compatibility.
/** Structured error from the render pipeline */
interface RenderError {
phase: 'instantiate' | 'render' | 'nested' | 'style' | 'serialize';
tagName: string;
message: string;
recoverable: boolean;
}
/** Hydration hint emitted during SSR for client-side adapter use */
interface HydrationHint {
tagName: string;
layer: ComponentLayer;
events?: HydrateEventDescriptor[];
strategy?: IslandUpgradeStrategy;
}
/** Structured output from renderDSD() */
interface RenderOutput {
html: string;
errors: RenderError[];
metrics: DsdRenderMetrics;
hydrationHints: HydrationHint[];
}
/** Adapter interface for framework-specific rendering */
interface RendererProtocol {
/** Adapter name for diagnostics and named lookup */
name: string;
/** Check if a value is a template type this adapter handles */
isTemplate?: (value: unknown) => boolean;
/** Render a template value to HTML string */
render?: (value: unknown, tagName: string) => Promise<string>;
/** Extract static CSS from a component class */
extractStyles?: (componentClass: CustomElementConstructor) => string | undefined;
}
/** Lifecycle hooks for the render pipeline */
interface RenderHooks {
beforeRender?(input: RenderInput): void | Promise<void>;
afterRender?(output: RenderOutput): void | Promise<void>;
onError?(error: RenderError): void | Promise<void>;
}
/** Input to a single renderDSD() call */
interface RenderInput {
tagName: string;
componentClass: CustomElementConstructor;
props: Record<string, unknown>;
dsdOptions?: DsdOptions;
nestingDepth: number;
}v0.15 status: Types (
RenderOutput,RenderError,HydrationHint) are defined and exported from@lessjs/core, butrenderDSD()still returnsPromise<string>. The return type change is deferred to v0.16 so it can be done together withRenderHooksintegration — avoiding two consecutive breaking changes to the same function signature.
The function signature changes from:
renderDSD(...): Promise<string>to:
renderDSD(...): Promise<RenderOutput>No backward-compatible wrapper is provided — LessJS is pre-1.0 and breaks are acceptable.
| File | Responsibility |
|---|---|
render-dsd.ts |
Pipeline orchestration + public API |
render-instantiate.ts |
Component instantiation + prop injection |
render-serialize.ts |
Attribute serialization + DSD template wrapping |
render-errors.ts |
Error types + RenderError classification |
v0.15 status:
DsdRenderCollectoris implemented andgetReport()returns structured metrics. However, the SSG pipeline does not yet writedsd-report.json. This is deferred to v0.16 because the report format depends onRenderOutput(§2), which is also deferred.
DsdRenderCollector.getReport() output is written to dsd-report.json during
SSG builds. The SSG CLI (adapter-vite/src/cli/ssg-render.ts) calls
Deno.writeTextFile() after all pages are rendered.
function registerAdapter(adapter: RendererProtocol | undefined): void;
function getAdapter(name?: string): RendererProtocol | undefined;
function getRegisteredAdapters(): readonly RendererProtocol[];The default adapter is the last one registered. Named lookup enables future multi-adapter scenarios (e.g., Lit for some components, vanilla for others).
- Fix JSR remote version resolution (handle API errors gracefully)
- Update scaffold template to reference
@lessjs/core@^0.15.0 - Add
app/routes/about.tstemplate for a more complete scaffold
Type changed from boolean | string to boolean to match the WHATWG HTML
Living Standard, where shadowrootcustomelementregistry is a boolean content
attribute with no value.
- Structured output enables tooling (diff reports, CI gates on error count, hydration optimization).
- Error taxonomy makes debugging SSR failures systematic.
- Module split makes
renderDSD()maintainable and testable. - DSD report gives build-time visibility into rendering performance.
- Named adapters unblock future multi-adapter support without committing to it now.
- Clean codebase with no dead backward-compatibility code.
- Breaking change to
renderDSD()return type. - Breaking change:
RenderAdapterremoved entirely — all adapters must implementRendererProtocol(addname: string). - Breaking change:
DsdOptions.customElementRegistryno longer accepts strings. render-dsd.tssplit touches a core hot path — requires careful regression testing.
RenderHooksare defined but not yet used in v0.15. They exist as extension points for v0.16 (package protocol observers).
- All existing tests pass after refactoring (54 tests, v0.15)
- Types for
RenderOutput,RenderError,HydrationHint,RenderInputdefined and exported -
renderDSD()returnsRenderOutputinstead ofstring(v0.16) -
dsd-report.jsonis generated duringdeno task build:ssg(v0.16) -
RenderHookswired into render pipeline (v0.16) -
deno run -A jsr:@lessjs/create test-appproduces a working scaffold -
deno task typecheck && deno lint && deno fmt --checkall pass - Zero
RenderAdapterreferences in codebase