diff --git a/packages/fresh/src/context.ts b/packages/fresh/src/context.ts index 362a515a3a9..3e1e59c164d 100644 --- a/packages/fresh/src/context.ts +++ b/packages/fresh/src/context.ts @@ -28,6 +28,8 @@ import { } from "./render.ts"; import { renderToString } from "preact-render-to-string"; +const ENCODER = new TextEncoder(); + export interface Island { file: string; name: string; @@ -258,11 +260,7 @@ export class Context { appVNode = appChild ?? h(Fragment, null); } - const headers = init.headers !== undefined - ? init.headers instanceof Headers - ? init.headers - : new Headers(init.headers) - : new Headers(); + const headers = getHeadersFromInit(init); headers.set("Content-Type", "text/html; charset=utf-8"); const responseInit: ResponseInit = { @@ -376,4 +374,102 @@ export class Context { }); return new Response(html, responseInit); } + + /** + * Respond with text. Sets `Content-Type: text/plain`. + * ```tsx + * app.use(ctx => ctx.text("Hello World!")); + * ``` + */ + text(content: string, init?: ResponseInit): Response { + return new Response(content, init); + } + + /** + * Respond with html string. Sets `Content-Type: text/html`. + * ```tsx + * app.get("/", ctx => ctx.html("

foo

")); + * ``` + */ + html(content: string, init?: ResponseInit): Response { + const headers = getHeadersFromInit(init); + headers.set("Content-Type", "text/html; charset=utf-8"); + + return new Response(content, { ...init, headers }); + } + + /** + * Respond with json string, same as `Response.json()`. Sets + * `Content-Type: application/json`. + * ```tsx + * app.get("/", ctx => ctx.json({ foo: 123 })); + * ``` + */ + // deno-lint-ignore no-explicit-any + json(content: any, init?: ResponseInit): Response { + return Response.json(content, init); + } + + /** + * Helper to stream a sync or async iterable and encode text + * automatically. + * + * ```tsx + * function* gen() { + * yield "foo"; + * yield "bar"; + * } + * + * app.use(ctx => ctx.stream(gen())) + * ``` + * + * Or pass in the function directly: + * + * ```tsx + * app.use(ctx => { + * return ctx.stream(function* gen() { + * yield "foo"; + * yield "bar"; + * }); + * ); + * ``` + */ + stream( + stream: + | Iterable + | AsyncIterable + | (() => Iterable | AsyncIterable), + init?: ResponseInit, + ): Response { + const raw = typeof stream === "function" ? stream() : stream; + + const body = ReadableStream.from(raw) + .pipeThrough( + new TransformStream({ + transform(chunk, controller) { + if (chunk instanceof Uint8Array) { + // deno-lint-ignore no-explicit-any + controller.enqueue(chunk as any); + } else if (chunk === undefined) { + controller.enqueue(undefined); + } else { + const raw = ENCODER.encode(String(chunk)); + controller.enqueue(raw); + } + }, + }), + ); + + return new Response(body, init); + } +} + +function getHeadersFromInit(init?: ResponseInit) { + if (init === undefined) { + return new Headers(); + } + + return init.headers !== undefined + ? init.headers instanceof Headers ? init.headers : new Headers(init.headers) + : new Headers(); } diff --git a/packages/fresh/src/context_test.tsx b/packages/fresh/src/context_test.tsx index de94d900e6a..b39dac1eb11 100644 --- a/packages/fresh/src/context_test.tsx +++ b/packages/fresh/src/context_test.tsx @@ -106,3 +106,100 @@ Deno.test("ctx.route - should contain matched route", async () => { await server.get("/foo/123"); expect(route).toEqual("/foo/:id"); }); + +Deno.test("ctx.text()", async () => { + const app = new App() + .get("/", (ctx) => ctx.text("foobar")); + + const server = new FakeServer(app.handler()); + const res = await server.get("/"); + + expect(res.headers.get("Content-Type")).toEqual("text/plain;charset=UTF-8"); + const text = await res.text(); + expect(text).toEqual("foobar"); +}); + +Deno.test("ctx.html()", async () => { + const app = new App() + .get("/", (ctx) => ctx.html("

foo

")); + + const server = new FakeServer(app.handler()); + const res = await server.get("/"); + + expect(res.headers.get("Content-Type")).toEqual("text/html; charset=utf-8"); + const text = await res.text(); + expect(text).toEqual("

foo

"); +}); + +Deno.test("ctx.json()", async () => { + const app = new App() + .get("/", (ctx) => ctx.json({ foo: 123 })); + + const server = new FakeServer(app.handler()); + const res = await server.get("/"); + + expect(res.headers.get("Content-Type")).toEqual("application/json"); + const text = await res.text(); + expect(text).toEqual('{"foo":123}'); +}); + +Deno.test("ctx.stream() - enqueue values", async () => { + function* gen() { + yield "foo"; + yield new TextEncoder().encode("bar"); + } + + const app = new App() + .get("/", (ctx) => ctx.stream(gen())); + + const server = new FakeServer(app.handler()); + const res = await server.get("/"); + const text = await res.text(); + expect(text).toEqual("foobar"); +}); + +Deno.test("ctx.stream() - pass function", async () => { + const app = new App() + .get("/", (ctx) => + ctx.stream(function* () { + yield "foo"; + yield "bar"; + })); + + const server = new FakeServer(app.handler()); + const res = await server.get("/"); + const text = await res.text(); + expect(text).toEqual("foobar"); +}); + +Deno.test("ctx.stream() - support iterable", async () => { + function* gen() { + yield "foo"; + yield "bar"; + } + + const app = new App() + .get("/", (ctx) => ctx.stream(gen())); + + const server = new FakeServer(app.handler()); + const res = await server.get("/"); + const text = await res.text(); + + expect(text).toEqual("foobar"); +}); + +Deno.test("ctx.stream() - support async iterable", async () => { + async function* gen() { + yield "foo"; + yield "bar"; + } + + const app = new App() + .get("/", (ctx) => ctx.stream(gen())); + + const server = new FakeServer(app.handler()); + const res = await server.get("/"); + const text = await res.text(); + + expect(text).toEqual("foobar"); +});