Skip to content

Commit 9ef84c7

Browse files
authored
Merge branch 'denoland:main' into feature/add_ip_restriction_middleware
2 parents 4c2f7a6 + a1000cb commit 9ef84c7

File tree

33 files changed

+417
-113
lines changed

33 files changed

+417
-113
lines changed

deno.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
{
2+
"vendor": true,
23
"nodeModulesDir": "manual",
34
"workspace": [
45
"./packages/*",

docs/latest/examples/daisyui.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ To get started with daisyUI, make sure you have Tailwind CSS enabled in your
1414
Fresh project, then install daisyUI and update your configuration.
1515

1616
1. Run `deno i -D npm:daisyui@latest` to install daisyUI
17-
2. Add daisyUI configuration in `./static/styles.css`:
17+
2. Add daisyUI configuration in `./assets/styles.css`:
1818

19-
```diff static/styles.css
19+
```diff assets/styles.css
2020
@import "tailwindcss";
2121
+ @plugin "daisyui";
2222
```

packages/examples/src/app1.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
1+
/**
2+
* Module containing a simple example Fresh App
3+
*
4+
* @module
5+
*/
6+
17
import { App } from "fresh";
28
import { Doc } from "./shared.tsx";
39

10+
/** App that renders a sample HTML document */
411
export const app1: App<unknown> = new App()
512
.get("/", (ctx) =>
613
ctx.render(

packages/examples/src/app2.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
1+
/**
2+
* Module containing a simple example Fresh App
3+
*
4+
* @module
5+
*/
6+
17
import { App } from "fresh";
28
import { Doc } from "./shared.tsx";
39

10+
/** App that renders a sample HTML document */
411
export const app2: App<unknown> = new App()
512
.get("/", (ctx) =>
613
ctx.render(

packages/examples/src/island.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,24 @@
1+
/**
2+
* Example of external Fresh Island component.
3+
*
4+
* @example
5+
* ```tsx
6+
* import { App } from "fresh";
7+
* import { DemoIsland } from "jsr:@fresh/examples/island";
8+
*
9+
* const app = new App();
10+
*
11+
* // Use the island somewhere in your components
12+
* app.get("/", (ctx) => ctx.render(<DemoIsland />));
13+
* ```
14+
*
15+
* @module
16+
*/
17+
118
import { useSignal } from "@preact/signals";
219
import type { JSX } from "preact";
320

21+
/** A simple counter demo island component using Preact signals */
422
export function DemoIsland(): JSX.Element {
523
const count = useSignal(0);
624

packages/fresh/README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,27 @@ Standards. It’s designed for building high-quality, performant, and personaliz
55
web applications.
66

77
[Learn more about Fresh](https://fresh.deno.dev/)
8+
9+
## Usage
10+
11+
Generate a new Fresh project with `@fresh/init`:
12+
13+
```sh
14+
deno run -Ar jsr:@fresh/init
15+
```
16+
17+
Add middleware, routes, & endpoints as needed via the `routes/` folder or
18+
directly on the `App` instance.
19+
20+
```tsx
21+
import { App } from "fresh";
22+
23+
const app = new App()
24+
.get("/", () => new Response("hello world"))
25+
.get("/jsx", (ctx) => ctx.render(<h1>render JSX!</h1>));
26+
27+
app.listen();
28+
```
29+
30+
For more information on getting started with Fresh, head on over to the
31+
[documentation](https://fresh.deno.dev/docs/introduction).

packages/fresh/deno.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@fresh/core",
3-
"version": "2.1.4",
3+
"version": "2.2.0",
44
"license": "MIT",
55
"exports": {
66
".": "./src/mod.ts",

packages/fresh/src/app.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,12 @@ export let setBuildCache: <State>(
154154
cache: BuildCache<State>,
155155
mode: "development" | "production",
156156
) => void;
157+
export let setErrorInterceptor: <State>(
158+
app: App<State>,
159+
fn: (err: unknown) => void,
160+
) => void;
161+
162+
const NOOP = () => {};
157163

158164
/**
159165
* Create an application instance that passes the incoming `Request`
@@ -162,6 +168,7 @@ export let setBuildCache: <State>(
162168
export class App<State> {
163169
#getBuildCache: () => BuildCache<State> | null = () => null;
164170
#commands: Command<State>[] = [];
171+
#onError: (err: unknown) => void = NOOP;
165172

166173
static {
167174
getBuildCache = (app) => app.#getBuildCache();
@@ -170,6 +177,9 @@ export class App<State> {
170177
app.config.mode = mode;
171178
app.#getBuildCache = () => cache;
172179
};
180+
setErrorInterceptor = (app, fn) => {
181+
app.#onError = fn;
182+
};
173183
}
174184

175185
/**
@@ -432,7 +442,7 @@ export class App<State> {
432442
try {
433443
if (handlers.length === 0) return await next();
434444

435-
const result = await runMiddlewares(handlers, ctx);
445+
const result = await runMiddlewares(handlers, ctx, this.#onError);
436446
if (!(result instanceof Response)) {
437447
throw new Error(
438448
`Expected a "Response" instance to be returned, but got: ${result}`,

packages/fresh/src/context.ts

Lines changed: 101 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ import {
2828
} from "./render.ts";
2929
import { renderToString } from "preact-render-to-string";
3030

31+
const ENCODER = new TextEncoder();
32+
3133
export interface Island {
3234
file: string;
3335
name: string;
@@ -258,11 +260,7 @@ export class Context<State> {
258260
appVNode = appChild ?? h(Fragment, null);
259261
}
260262

261-
const headers = init.headers !== undefined
262-
? init.headers instanceof Headers
263-
? init.headers
264-
: new Headers(init.headers)
265-
: new Headers();
263+
const headers = getHeadersFromInit(init);
266264

267265
headers.set("Content-Type", "text/html; charset=utf-8");
268266
const responseInit: ResponseInit = {
@@ -376,4 +374,102 @@ export class Context<State> {
376374
});
377375
return new Response(html, responseInit);
378376
}
377+
378+
/**
379+
* Respond with text. Sets `Content-Type: text/plain`.
380+
* ```tsx
381+
* app.use(ctx => ctx.text("Hello World!"));
382+
* ```
383+
*/
384+
text(content: string, init?: ResponseInit): Response {
385+
return new Response(content, init);
386+
}
387+
388+
/**
389+
* Respond with html string. Sets `Content-Type: text/html`.
390+
* ```tsx
391+
* app.get("/", ctx => ctx.html("<h1>foo</h1>"));
392+
* ```
393+
*/
394+
html(content: string, init?: ResponseInit): Response {
395+
const headers = getHeadersFromInit(init);
396+
headers.set("Content-Type", "text/html; charset=utf-8");
397+
398+
return new Response(content, { ...init, headers });
399+
}
400+
401+
/**
402+
* Respond with json string, same as `Response.json()`. Sets
403+
* `Content-Type: application/json`.
404+
* ```tsx
405+
* app.get("/", ctx => ctx.json({ foo: 123 }));
406+
* ```
407+
*/
408+
// deno-lint-ignore no-explicit-any
409+
json(content: any, init?: ResponseInit): Response {
410+
return Response.json(content, init);
411+
}
412+
413+
/**
414+
* Helper to stream a sync or async iterable and encode text
415+
* automatically.
416+
*
417+
* ```tsx
418+
* function* gen() {
419+
* yield "foo";
420+
* yield "bar";
421+
* }
422+
*
423+
* app.use(ctx => ctx.stream(gen()))
424+
* ```
425+
*
426+
* Or pass in the function directly:
427+
*
428+
* ```tsx
429+
* app.use(ctx => {
430+
* return ctx.stream(function* gen() {
431+
* yield "foo";
432+
* yield "bar";
433+
* });
434+
* );
435+
* ```
436+
*/
437+
stream<U extends string | Uint8Array>(
438+
stream:
439+
| Iterable<U>
440+
| AsyncIterable<U>
441+
| (() => Iterable<U> | AsyncIterable<U>),
442+
init?: ResponseInit,
443+
): Response {
444+
const raw = typeof stream === "function" ? stream() : stream;
445+
446+
const body = ReadableStream.from(raw)
447+
.pipeThrough(
448+
new TransformStream({
449+
transform(chunk, controller) {
450+
if (chunk instanceof Uint8Array) {
451+
// deno-lint-ignore no-explicit-any
452+
controller.enqueue(chunk as any);
453+
} else if (chunk === undefined) {
454+
controller.enqueue(undefined);
455+
} else {
456+
const raw = ENCODER.encode(String(chunk));
457+
controller.enqueue(raw);
458+
}
459+
},
460+
}),
461+
);
462+
463+
return new Response(body, init);
464+
}
465+
}
466+
467+
function getHeadersFromInit(init?: ResponseInit) {
468+
if (init === undefined) {
469+
return new Headers();
470+
}
471+
472+
return init.headers !== undefined
473+
? init.headers instanceof Headers ? init.headers : new Headers(init.headers)
474+
: new Headers();
379475
}

packages/fresh/src/context_test.tsx

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,3 +106,100 @@ Deno.test("ctx.route - should contain matched route", async () => {
106106
await server.get("/foo/123");
107107
expect(route).toEqual("/foo/:id");
108108
});
109+
110+
Deno.test("ctx.text()", async () => {
111+
const app = new App()
112+
.get("/", (ctx) => ctx.text("foobar"));
113+
114+
const server = new FakeServer(app.handler());
115+
const res = await server.get("/");
116+
117+
expect(res.headers.get("Content-Type")).toEqual("text/plain;charset=UTF-8");
118+
const text = await res.text();
119+
expect(text).toEqual("foobar");
120+
});
121+
122+
Deno.test("ctx.html()", async () => {
123+
const app = new App()
124+
.get("/", (ctx) => ctx.html("<h1>foo</h1>"));
125+
126+
const server = new FakeServer(app.handler());
127+
const res = await server.get("/");
128+
129+
expect(res.headers.get("Content-Type")).toEqual("text/html; charset=utf-8");
130+
const text = await res.text();
131+
expect(text).toEqual("<h1>foo</h1>");
132+
});
133+
134+
Deno.test("ctx.json()", async () => {
135+
const app = new App()
136+
.get("/", (ctx) => ctx.json({ foo: 123 }));
137+
138+
const server = new FakeServer(app.handler());
139+
const res = await server.get("/");
140+
141+
expect(res.headers.get("Content-Type")).toEqual("application/json");
142+
const text = await res.text();
143+
expect(text).toEqual('{"foo":123}');
144+
});
145+
146+
Deno.test("ctx.stream() - enqueue values", async () => {
147+
function* gen() {
148+
yield "foo";
149+
yield new TextEncoder().encode("bar");
150+
}
151+
152+
const app = new App()
153+
.get("/", (ctx) => ctx.stream(gen()));
154+
155+
const server = new FakeServer(app.handler());
156+
const res = await server.get("/");
157+
const text = await res.text();
158+
expect(text).toEqual("foobar");
159+
});
160+
161+
Deno.test("ctx.stream() - pass function", async () => {
162+
const app = new App()
163+
.get("/", (ctx) =>
164+
ctx.stream(function* () {
165+
yield "foo";
166+
yield "bar";
167+
}));
168+
169+
const server = new FakeServer(app.handler());
170+
const res = await server.get("/");
171+
const text = await res.text();
172+
expect(text).toEqual("foobar");
173+
});
174+
175+
Deno.test("ctx.stream() - support iterable", async () => {
176+
function* gen() {
177+
yield "foo";
178+
yield "bar";
179+
}
180+
181+
const app = new App()
182+
.get("/", (ctx) => ctx.stream(gen()));
183+
184+
const server = new FakeServer(app.handler());
185+
const res = await server.get("/");
186+
const text = await res.text();
187+
188+
expect(text).toEqual("foobar");
189+
});
190+
191+
Deno.test("ctx.stream() - support async iterable", async () => {
192+
async function* gen() {
193+
yield "foo";
194+
yield "bar";
195+
}
196+
197+
const app = new App()
198+
.get("/", (ctx) => ctx.stream(gen()));
199+
200+
const server = new FakeServer(app.handler());
201+
const res = await server.get("/");
202+
const text = await res.text();
203+
204+
expect(text).toEqual("foobar");
205+
});

0 commit comments

Comments
 (0)