npm package: @superhighway/silk
Description: Silk is an embedded DSL for authoring HTML from TypeScript. You write simple typed [JSX][jsx] and Silk generates [`ReadableStream`s][readable-stream] of [`HTMLToken`s][html-tokens].
Latest version: 0.3.1

# silk

Silk is an embedded DSL for authoring HTML from TypeScript. You write simple
typed [JSX][jsx] and Silk generates [`ReadableStream`s][readable-stream] of
[`HTMLToken`s][html-tokens].

Child nodes and attributes can be async values or streams.

Here's an example:

```tsx
import { createElement, type ReadableHTMLStream } from '@superhighway/silk'

const slowlyGetPlanet = () =>
  // Imagine this queries a database or some third-party API.
  new Promise<ReadableHTMLStream>(resolve =>
    setTimeout(() => resolve(<strong>world</strong>), 2000),
  )

export default () => (
  <html lang="en">
    <head>
      <title>Greeting</title>
    </head>
    <body>Hello, {slowlyGetPlanet()}!</body>
  </html>
)
```

The HTML structure and content before the `slowlyGetPlanet` call will
immediately be readable from the stream, while the rest will appear as soon as
the `Promise` returned by `slowlyGetPlanet` resolves.

## Setup

To use Silk, add these options to your `tsconfig.json`[^1]:
```json
"jsx": "react",
"jsxFactory": "createElement",
"jsxFragmentFactory": "createElement",
```

Also, `import { createElement } from '@superhighway/silk'` in each of your
`.tsx` files.

## Server-Side Usage

If you're using Silk for server-side rendering and want a stream to pipe out as
the HTTP response, `HTMLSerializingTransformStream` has you covered. Here's an
example of an HTTP server which uses Silk to serve a web page:

```tsx
import { createServer } from 'node:http'
import { Writable } from 'node:stream'
import {
  type ReadableHTMLStream,
  createElement,
  HTMLSerializingTransformStream,
} from '@superhighway/silk'

const port = 80

createServer((_request, response) => {
  const document = (
    <html lang="en">
      <head>
        <title>Greeting</title>
      </head>
      <body>Hello, {slowlyGetPlanet()}!</body>
    </html>
  )

  response.setHeader('Content-Type', 'text/html; charset=utf-8')
  document
    .pipeThrough(
      new HTMLSerializingTransformStream({
        includeDoctype: true,
      }),
    )
    .pipeTo(Writable.toWeb(response))
    .catch(console.error)
}).listen(port)

const slowlyGetPlanet = () =>
  new Promise<ReadableHTMLStream>(resolve =>
    setTimeout(() => resolve(<strong>world</strong>), 2000),
  )
```

If you run that and make a request to it from a web browser, you'll see "Hello,
" appear quickly, then "world!" appear after two seconds. You can [try it on
StackBlitz][silk-example-server-stackblitz].

For a somewhat more featureful web server to use with Silk, see [Loom][loom],
and for more elaborate examples, see <https://github.com/mkantor/silk-demos>.

## Client-Side Usage

Silk can also be used client-side by translating the stream of
[`HTMLToken`s][html-tokens] into DOM method calls. Silk exports a
`consumeAsDOMChildren` helper function to make this straightforward; here's an
example:

```tsx
import {
  createElement,
  consumeAsDOMChildren,
  type ReadableHTMLStream,
} from '@superhighway/silk'

const slowlyGetPlanet = () =>
  new Promise<ReadableHTMLStream>(resolve =>
    setTimeout(() => resolve(<strong>world</strong>), 2000),
  )

const container = document.getElementById('app')
if (container === null) {
  throw new Error('Container element does not exist')
}

await consumeAsDOMChildren(container, <>Hello, {slowlyGetPlanet()}!</>)
```

You can [try this on StackBlitz][silk-example-client-stackblitz].

## Why Streaming?

HTML is inherently streamable, yet many web servers buffer the entire response
body before sending a single byte of it to the client. This leaves performance
on the table—web browsers are perfectly capable of incrementally parsing and
rendering partial HTML documents as they arrive.

Streaming is especially valuable when the document references external resources
(e.g. stylesheets). By sending HTML to the client while the server continues
asynchronous work, the browser can fetch those resources concurrently with that
work, significantly reducing the time required to display the page.

Streaming also helps keep server memory utilization low as data already sent to
the client can be freed.

## It's Just HTML

### Attributes

There are no non-HTML attributes (like `ref` and `key`), attribute names are
always spelled exactly as they are in HTML (e.g. `class` is not `className`),
and attribute values are plain strings.

### Components

Silk doesn't have "components" as part of its JSX syntax—all elements are
intrinsic. If you want to abstract/reuse bits of HTML, you can call functions
from within `{…}` blocks:

```tsx
<div>{profile(userID)}</div>
```

### Typing

JSX elements are strictly-typed, with each element only accepting its known
attributes, attributes only accepting known values, and [void
elements][void-elements] forbidding children. Tag names and attributes are
suggested in completions, and documentation from [MDN][mdn] is presented in type
info:

![Screenshot of type info for a JSX
element](./media/element-type-info-screenshot.png)

[^1]: `"jsx": "react"` may seem odd because Silk isn't related to React, but
TypeScript's JSX configuration is based around React's semantics.

[jsx]: https://facebook.github.io/jsx/
[loom]: https://github.com/mkantor/loom
[readable-stream]: https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream
[mdn]: https://developer.mozilla.org/
[html-tokens]: ./src/htmlToken.ts
[silk-example-server-stackblitz]: https://stackblitz.com/edit/silk-example-server?file=src%2Findex.tsx
[silk-example-client-stackblitz]: https://stackblitz.com/edit/silk-example-client?file=index.html,src%2Findex.tsx
[void-elements]: https://html.spec.whatwg.org/#void-elements