Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
106 changes: 101 additions & 5 deletions packages/fresh/src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -258,11 +260,7 @@ export class Context<State> {
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 = {
Expand Down Expand Up @@ -376,4 +374,102 @@ export class Context<State> {
});
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("<h1>foo</h1>"));
* ```
*/
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<U extends string | Uint8Array>(
stream:
| Iterable<U>
| AsyncIterable<U>
| (() => Iterable<U> | AsyncIterable<U>),
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();
}
97 changes: 97 additions & 0 deletions packages/fresh/src/context_test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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("<h1>foo</h1>"));

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("<h1>foo</h1>");
});

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");
});