Skip to content

feat: add @taujs/html - framework-free HTML SSR and streaming renderer - #169

Merged
aoede3 merged 9 commits into
mainfrom
feat/html-renderer
Sep 7, 2026
Merged

aoede3 merged 9 commits into
mainfrom
feat/html-renderer

Conversation

@aoede3

@aoede3 aoede3 commented Sep 7, 2026

Copy link
Copy Markdown
Owner

Adds @taujs/html, a fourth first-party renderer for apps with no component framework.

  • htmlRenderer() from @taujs/html/renderer (key html, no compiler, empty plugin pack)
  • createRenderer({ render? }) returns raw { headContent, appHtml }; zero-config form is empty fragments
  • No templating or escaping helpers: the application owns its HTML safety
  • Streaming delivers deferred route data as data under a mandatory finite deferredTimeoutMs; the client reads it through the single onDataReady export in @taujs/html/client
  • Server-side projection of deferred entries is out of scope for this release
  • Server change is diagnostic strings only: the required-renderer message names all four factories
  • New fixture playground-html with fetch-driven route cells and a real-browser deferred cell
  • Docs page renderers/html plus config reference, getting-started and README rows

Changesets: @taujs/html minor (first publish, 0.1.0), @taujs/server patch.

New package: no component framework, no compiler, no templating or escaping helper - render()
returns raw { headContent, appHtml } written to the response verbatim, so the application owns
its own HTML safety. Streaming carries the same abort handling, pre-observed done, exactly-once
onError and terminal-guard discipline as the framework renderers; deferred route data is
delivered as data only (never projected into HTML), bounded by a mandatory finite
deferredTimeoutMs since service calls carry no automatic deadline of their own. The client reads
that envelope through the single onDataReady export in @taujs/html/client.

Modelled on @taujs/vue's streaming machinery and render-contract brand; htmlRenderer() from
@taujs/html/renderer supplies an empty environment-plugin pack (no compiler to own).
Diagnostic strings and comments only, no behavioural change. The required-renderer message and
the other runtime strings that used the two-name idiom (reactRenderer()/vueRenderer()) now name
all four first-party factories in the same order, following the addition of @taujs/html; comments
that named specific factories now say "a renderer factory" generically. One new cell asserts the
required-renderer message names every first-party factory.
Modelled on fixtures/playground-vue: one bootable app that exercises @taujs/html end to end
against the workspace package. Four routes cover ssr, streaming, deferred-with-hydrate and
deferred-without-hydrate; the entry-server escapes its own interpolations locally (@taujs/html
provides no escaping helper) and sets a 500ms deferredTimeoutMs so the deadline is exercised
within the test suite's own timeout. test/routes.test.ts boots the real built prod and dev
servers as child processes and asserts over plain fetch; test/browser.test.ts drives the deferred
route in a real Chromium (skips visibly when the pinned browser is absent).
New renderers/html.mdx page (raw-HTML callout first, before any example), listed beside the
Solid page in the sidebar. The config reference's renderer row and the getting-started guide's
renderer sentence now name htmlRenderer(). Root README gains the npm badge, the package table row
and the repository-tree entries for @taujs/html and its fixture.
@taujs/html minor (first publication, 0.1.0); @taujs/server patch (renderer diagnostics name all
four factories).
…act lists

RendererContract.ts:9,99 named reactRenderer()/vueRenderer() specifically; the implementation
contract's section 5 lists both as comment sites that should say "a renderer factory" generically,
alongside the diagnostic-string sweep in Config.ts/OwnershipPrepass.ts/ManagedPlugins.ts. No
behavioural change.
Contract section 4.3 names four race outcomes the terminal guards make mechanically true; none
of them existed as separate cells. Added, titled as the claims they assert:

(a) abort while the critical-data thunk (not render) is pending: onHead never called, nothing
    written, done resolves.
(b) a shell-timer expiry while render is pending: onError exactly once, done rejects; a LATER
    render resolution produces no write and no callback.
(c) abort during the deferred wait (a never-settling entry, abort after onHead lands on a
    macrotask so it lands inside the Promise.race, not synchronously inside onShellReady): no
    onAllReady, no bootstrap tag, end() never called, done resolves.
(d) onShellReady aborting the signal synchronously: no appHtml write, no bootstrap tag, no
    end(), done resolves.

All four passed against the existing implementation unmodified - the terminal guards already
described in SSRRender.ts were correct; only the pinning cells were missing.
… after every write

Three empty catch blocks around writable.write(appHtml), the bootstrap-tag write and
writable.end() swallowed synchronous sink exceptions instead of letting them reach the run()
task's outer catch and fail() - a throwing write silently lost content with done resolved clean,
and a throwing end() left done pending forever with no 'finish' ever able to fire. Removed all
three; a synchronous throw now goes through fail() exactly once, same as any other step failure.

Also added `if (controller.isAborted) return;` immediately after the appHtml write and again
after the bootstrap-tag write: a write that synchronously emits a fatal sink 'error' (handled by
the writable guards, which already call fail()) was previously falling through to onAllReady
and the bootstrap tag even though the controller had already claimed a fatal outcome.

Three regression cells added to Streaming.test.ts, titled as the claims they assert.
The README's quick-start example interpolated meta.title and data.message with String(...) right
under a warning that the application must escape - String() escapes nothing. Replaced it with the
same three-line escaper fixtures/playground-html/src/client/entry-server.ts uses, applied to both
interpolations, with a comment stating @taujs/html provides no escaper.

Also added a one-line comment directly on the website's createRenderer example (html.mdx) making
explicit that `escape` there is the application's own function, not a @taujs/html export.
@aoede3
aoede3 merged commit ad30dbe into main Sep 7, 2026
2 checks passed
@aoede3
aoede3 deleted the feat/html-renderer branch September 7, 2026 13:48
@github-actions github-actions Bot mentioned this pull request Sep 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant