Skip to content

Commit cdb5619

Browse files
feat: lib support for streams and fixed headers
1 parent b031c96 commit cdb5619

File tree

4 files changed

+116
-49
lines changed

4 files changed

+116
-49
lines changed

src/handler.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { Readable } from 'stream'
2+
import { HtmlOptions } from './options'
3+
import { isHtml, isTagHtml } from './utils'
4+
5+
export function handleHtml(
6+
value: string | Readable | Promise<string | Readable>,
7+
options: HtmlOptions,
8+
hasContentType: boolean
9+
): Promise<Response | string> | Response | string {
10+
// Only use promises if value is a promise itself
11+
if (value instanceof Promise) {
12+
return value.then((v) => handleHtml(v, options, hasContentType))
13+
}
14+
15+
// Simple string use cases
16+
if (typeof value === 'string') {
17+
if (
18+
options.autoDoctype &&
19+
isHtml(value) &&
20+
// Avoids double adding !doctype or adding to non root html tags.
21+
isTagHtml(value)
22+
) {
23+
value = '<!doctype html>' + value
24+
}
25+
26+
return new Response(
27+
value,
28+
hasContentType
29+
? undefined
30+
: { headers: { 'content-type': options.contentType! } }
31+
)
32+
}
33+
34+
// Stream use cases
35+
let stream = Readable.toWeb(value)
36+
37+
// We can convert to a readable stream with StreamTransform
38+
if (options.autoDoctype) {
39+
let first = true
40+
41+
stream = stream.pipeThrough(
42+
new TransformStream({
43+
transform(chunk, controller) {
44+
let str = chunk!.toString()
45+
46+
if (
47+
first &&
48+
isTagHtml(str) &&
49+
// Avoids double adding !doctype or adding to non root html tags.
50+
isTagHtml(str)
51+
) {
52+
first = false
53+
str = '<!doctype html>' + str
54+
}
55+
56+
controller.enqueue(str)
57+
}
58+
})
59+
)
60+
}
61+
62+
return new Response(
63+
stream,
64+
hasContentType
65+
? undefined
66+
: { headers: { 'content-type': options.contentType! } }
67+
)
68+
}

src/html.ts

Lines changed: 37 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import { Elysia } from 'elysia'
2-
import { isHtml, isTagHtml } from './utils'
2+
import { Readable } from 'stream'
3+
import { handleHtml } from './handler'
34
import { HtmlOptions } from './options'
5+
import { isHtml } from './utils'
6+
import { renderToStream } from '@kitajs/html/suspense'
47

58
export function html(options: HtmlOptions = {}) {
69
// Defaults
@@ -9,48 +12,44 @@ export function html(options: HtmlOptions = {}) {
912
options.isHtml ??= isHtml
1013
options.autoDoctype ??= true
1114

12-
let instance = new Elysia({ name: '@elysiajs/html' }).derive(({ set }) => ({
13-
html(value: string) {
14-
if (
15-
options.autoDoctype &&
16-
isHtml(value) &&
17-
// Avoids double adding !doctype or adding to non root html tags.
18-
isTagHtml(value)
19-
) {
20-
value = '<!doctype html>' + value
21-
}
15+
let instance = new Elysia({ name: '@elysiajs/html' }).derive(({ set }) => {
16+
return {
17+
html<A = void>(
18+
value:
19+
| Readable
20+
| JSX.Element
21+
| ((this: void, rid: number, ...args: A[]) => JSX.Element),
22+
...args: A[]
23+
): Promise<Response | string> | Response | string {
24+
if (typeof value === 'function') {
25+
value = renderToStream((rid) =>
26+
(value as Function)(rid, ...args)
27+
)
28+
}
2229

23-
return new Response(value, {
24-
...set,
25-
// @ts-expect-error
26-
headers: { ...set.headers, 'content-type': options.contentType! },
27-
})
30+
return handleHtml(value, options, 'content-type' in set.headers)
31+
}
2832
}
29-
}))
33+
})
3034

3135
if (options.autoDetect) {
32-
instance = instance.onAfterHandle(
33-
// onAfterHandle should be present on a lot of stack traces, so we should not
34-
// use anonymous functions here.
35-
function htmlHandle({ set }, response) {
36-
if (!isHtml(response)) {
37-
return response
38-
}
39-
40-
// Full means that we should only try to convert raw string responses
41-
if (options.autoDoctype === 'full' && isTagHtml(response)) {
42-
response = '<!doctype html>' + response
43-
}
44-
45-
set.headers['content-type'] = options.contentType!
46-
47-
return new Response(
48-
// @ts-expect-error - We know this is a string.
49-
response,
50-
set
51-
)
36+
// handlerPossibleHtml should be present on a lot of stack traces, so we should not
37+
// use anonymous functions here.
38+
instance = instance.onAfterHandle(function handlerPossibleHtml({
39+
response: value,
40+
set
41+
}) {
42+
if (
43+
// Simple html string
44+
isHtml(value) ||
45+
// @kitajs/html stream
46+
(value instanceof Readable && 'rid' in value)
47+
) {
48+
return handleHtml(value, options, 'content-type' in set.headers)
5249
}
53-
)
50+
51+
return value
52+
})
5453
}
5554

5655
return instance

src/options.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,16 +21,16 @@ export interface HtmlOptions {
2121
/**
2222
* Whether to automatically add `<!doctype html>` to a response starting with <html>, if not found.
2323
*
24-
* Use `full` to also automatically add doctypes on responses returned without this plugin
25-
*
26-
* ```ts
27-
* // without the plugin
28-
* app.get('/', () => '<html></html>')
29-
*
30-
* // With the plugin
31-
* app.get('/', ({ html }) => html('<html></html>')
32-
* ```
33-
*
24+
* Use `full` to also automatically add doctypes on responses returned without this plugin
25+
*
26+
* ```ts
27+
* // without the plugin
28+
* app.get('/', () => '<html></html>')
29+
*
30+
* // With the plugin
31+
* app.get('/', ({ html }) => html('<html></html>')
32+
* ```
33+
*
3434
* @default true
3535
*/
3636
autoDoctype?: boolean | 'full'

src/utils.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,5 @@ export function isHtml(this: void, value?: any): value is string {
2727
* casing**.
2828
*/
2929
export function isTagHtml(this: void, value: string) {
30-
return value.trimStart().slice(0, 5).startsWith('<html')
30+
return value.trimStart().slice(0, 5).startsWith('<html')
3131
}

0 commit comments

Comments
 (0)