Skip to content

Commit

Permalink
Hono integration
Browse files Browse the repository at this point in the history
Close #25
  • Loading branch information
dahlia committed Apr 2, 2024
1 parent 46d75e2 commit f20756a
Show file tree
Hide file tree
Showing 12 changed files with 276 additions and 0 deletions.
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"Guppe",
"halfyear",
"hongminhee",
"hono",
"httpsig",
"jsonld",
"keypair",
Expand Down
8 changes: 8 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@ To be released.
- Added `integrateFetchOptions()` function.
- Deprecated `integrateHandlerOptions()` function.

- Added `@fedify/fedify/x/hono` module for integrating with [Hono] middleware.
[[#25]]

- Added `federation()` function.
- Added `ContextDataFactory` type.

[Hono]: https://hono.dev/
[#25]: https://github.com/dahlia/fedify/issues/25
[#27]: https://github.com/dahlia/fedify/issues/27


Expand Down
1 change: 1 addition & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ added in the future.[^1]
- [Actor lookup CLI](./actor-lookup-cli/)
- [Federated single-user blog](./blog/)
- [Fedi badge](https://github.com/dahlia/fedi-badge)
- [Hono integration sample](./hono-sample/)

[^1]: Contributions are welcome! If you have built an application with the
Fedify framework and want to share it with others, please consider adding
Expand Down
5 changes: 5 additions & 0 deletions examples/hono-sample/.vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"recommendations": [
"denoland.vscode-deno"
]
}
41 changes: 41 additions & 0 deletions examples/hono-sample/.vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"deno.enable": true,
"deno.lint": true,
"files.eol": "\n",
"files.insertFinalNewline": true,
"files.trimFinalNewlines": true,
"git.openRepositoryInParentFolders": "never",
"[javascript]": {
"editor.defaultFormatter": "denoland.vscode-deno",
"editor.formatOnSave": true
},
"[javascriptreact]": {
"editor.defaultFormatter": "denoland.vscode-deno",
"editor.formatOnSave": true
},
"[json]": {
"editor.defaultFormatter": "vscode.json-language-features",
"editor.formatOnSave": true
},
"[jsonc]": {
"editor.defaultFormatter": "vscode.json-language-features",
"editor.formatOnSave": true
},
"[typescript]": {
"editor.defaultFormatter": "denoland.vscode-deno",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.sortImports": "always"
}
},
"[typescriptreact]": {
"editor.defaultFormatter": "denoland.vscode-deno",
"editor.formatOnSave": true
},
"cSpell.words": [
"deno",
"fedi",
"fedify",
"hono"
]
}
15 changes: 15 additions & 0 deletions examples/hono-sample/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
Hono integration sample
=======================

This project is a sample application that demonstrates how to integrate Fedify
with [Hono], a web server framework in TypeScript.

To run the sample:

~~~~ command
deno task start
~~~~

The sample application will be available at <http://localhost:8000/>.

[Hono]: https://hono.dev/
8 changes: 8 additions & 0 deletions examples/hono-sample/deno.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"importMap": "./import_map.g.json",
"tasks": {
"generate-import-map": "deno run --allow-read --allow-write generate_import_map.ts",
"codegen": "deno task generate-import-map",
"start": "deno task codegen && deno run -A main.ts"
}
}
14 changes: 14 additions & 0 deletions examples/hono-sample/generate_import_map.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
const fedifyImportMap = JSON.parse(
await Deno.readTextFile(`${import.meta.dirname}/../../deno.json`),
).imports;

const blogImportMap = JSON.parse(
await Deno.readTextFile(`${import.meta.dirname}/import_map.json`),
).imports;

const importMap = { ...fedifyImportMap, ...blogImportMap };

await Deno.writeTextFile(
`${import.meta.dirname}/import_map.g.json`,
JSON.stringify({ imports: importMap }, null, 2) + "\n",
);
41 changes: 41 additions & 0 deletions examples/hono-sample/import_map.g.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"imports": {
"@cfworker/json-schema": "npm:@cfworker/json-schema@^1.12.8",
"@david/which-runtime": "jsr:@david/which-runtime@^0.2.0",
"@deno/dnt": "jsr:@deno/dnt@^0.41.1",
"@fedify/fedify": "../../mod.ts",
"@fedify/fedify/federation": "../../federation/mod.ts",
"@fedify/fedify/httpsig": "../../httpsig/mod.ts",
"@fedify/fedify/nodeinfo": "./nodeinfo/mod.ts",
"@fedify/fedify/runtime": "../../runtime/mod.ts",
"@fedify/fedify/vocab": "../../vocab/mod.ts",
"@fedify/fedify/webfinger": "../../webfinger/mod.ts",
"@fedify/fedify/x/fresh": "./x/fresh.ts",
"@hongminhee/aitertools": "jsr:@hongminhee/aitertools@^0.6.0",
"@js-temporal/polyfill": "npm:@js-temporal/polyfill@^0.4.4",
"@phensley/language-tag": "npm:@phensley/language-tag@^1.8.0",
"@std/assert": "jsr:@std/assert@^0.220.1",
"@std/async/delay": "jsr:@std/async@^0.220.1/delay",
"@std/bytes": "jsr:@std/bytes@^0.220.1",
"@std/collections": "jsr:@std/collections@^0.220.1",
"@std/encoding": "jsr:@std/encoding@^0.220.1",
"@std/encoding/base64": "jsr:@std/encoding@^0.220.1/base64",
"@std/fs": "jsr:@std/fs@^0.220.1",
"@std/http/negotiation": "jsr:@std/http@^0.220.1/negotiation",
"@std/json/common": "jsr:@std/json@^0.220.1/common",
"@std/path": "jsr:@std/path@^0.220.1",
"@std/semver": "jsr:@std/semver@^0.220.1",
"@std/testing": "jsr:@std/testing@^0.220.1",
"@std/text": "jsr:@std/text@^0.220.1",
"@std/url": "jsr:@std/url@^0.220.1",
"@std/yaml": "jsr:@std/yaml@^0.220.1",
"fast-check": "npm:fast-check@^3.17.0",
"jsonld": "npm:jsonld@^8.3.2",
"mock_fetch": "https://deno.land/x/[email protected]/mod.ts",
"uri-template-router": "npm:uri-template-router@^0.0.16",
"url-template": "npm:url-template@^3.1.1",
"@fedify/fedify/x/denokv": "../../x/denokv.ts",
"@fedify/fedify/x/hono": "../../x/hono.ts",
"hono": "https://deno.land/x/[email protected]/mod.ts"
}
}
13 changes: 13 additions & 0 deletions examples/hono-sample/import_map.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"imports": {
"@fedify/fedify": "../../mod.ts",
"@fedify/fedify/federation": "../../federation/mod.ts",
"@fedify/fedify/httpsig": "../../httpsig/mod.ts",
"@fedify/fedify/runtime": "../../runtime/mod.ts",
"@fedify/fedify/vocab": "../../vocab/mod.ts",
"@fedify/fedify/webfinger": "../../webfinger/mod.ts",
"@fedify/fedify/x/denokv": "../../x/denokv.ts",
"@fedify/fedify/x/hono": "../../x/hono.ts",
"hono": "https://deno.land/x/[email protected]/mod.ts"
}
}
26 changes: 26 additions & 0 deletions examples/hono-sample/main.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Federation, MemoryKvStore } from "@fedify/fedify/federation";
import { Person } from "@fedify/fedify/vocab";
import { federation } from "@fedify/fedify/x/hono";
import { Hono } from "hono";

const fedi = new Federation<void>({
kv: new MemoryKvStore(),
});

fedi.setActorDispatcher("/{handle}", (ctx, handle, _key) => {
if (handle !== "sample") return null;
return new Person({
id: ctx.getActorUri(handle),
name: "Sample",
preferredUsername: handle,
});
});

const app = new Hono();
app.use(federation(fedi, () => undefined));
app.get("/", (c) => c.redirect("/sample"));
app.get("/sample", (c) => c.text("Hi, I am Sample!\n"));

if (import.meta.main) Deno.serve(app.fetch.bind(app));

export default app;
103 changes: 103 additions & 0 deletions x/hono.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/**
* Fedify with Hono
* ================
*
* This module provides a [Hono] middleware to integrate with the Fedify.
*
* [Hono]: https://hono.dev/
*
* @module
* @since 0.6.0
*/
import type {
Federation,
FederationFetchOptions,
} from "../federation/middleware.ts";

interface HonoRequest {
raw: Request;
}

interface HonoContext {
req: HonoRequest;
res: Response;
}

type HonoMiddleware<THonoContext extends HonoContext> = (
ctx: THonoContext,
next: () => Promise<void>,
) => Promise<Response | void>;

/**
* A factory function to create a context data for the {@link Federation}
* object.
*
* @typeParam TContextData A type of the context data for the {@link Federation}
* object.
* @typeParam THonoContext A type of the Hono context.
* @param context A Hono context object.
* @returns A context data for the {@link Federation} object.
*/
export type ContextDataFactory<TContextData, THonoContext> = (
context: THonoContext,
) => TContextData | Promise<TContextData>;

/**
* Create a Hono middleware to integrate with the {@link Federation} object.
*
* @typeParam TContextData A type of the context data for the {@link Federation}
* object.
* @typeParam THonoContext A type of the Hono context.
* @param federation A {@link Federation} object to integrate with Hono.
* @param contextDataFactory A function to create a context data for the
* {@link Federation} object.
* @returns A Hono middleware.
*/
export function federation<TContextData, THonoContext extends HonoContext>(
federation: Federation<TContextData>,
contextDataFactory: ContextDataFactory<TContextData, THonoContext>,
): HonoMiddleware<THonoContext> {
return async (ctx, next) => {
let contextData = contextDataFactory(ctx);
if (contextData instanceof Promise) contextData = await contextData;
return await federation.fetch(ctx.req.raw, {
contextData,
...integrateFetchOptions(ctx, next),
});
};
}

function integrateFetchOptions<THonoContext extends HonoContext>(
ctx: THonoContext,
next: () => Promise<void>,
): Omit<FederationFetchOptions<void>, "contextData"> {
return {
// If the `federation` object finds a request not responsible for it
// (i.e., not a federation-related request), it will call the `next`
// provided by the Hono framework to continue the request handling
// by the Hono:
async onNotFound(_req: Request): Promise<Response> {
await next();
return ctx.res;
},

// Similar to `onNotFound`, but slightly more tricky one.
// When the `federation` object finds a request not acceptable type-wise
// (i.e., a user-agent doesn't want JSON-LD), it will call the `next`
// provided by the Hono framework so that it renders HTML if there's some
// page. Otherwise, it will simply return a 406 Not Acceptable response.
// This kind of trick enables the Fedify and Hono to share the same routes
// and they do content negotiation depending on `Accept` header:
async onNotAcceptable(_req: Request): Promise<Response> {
await next();
if (ctx.res.status !== 404) return ctx.res;
return new Response("Not acceptable", {
status: 406,
headers: {
"Content-Type": "text/plain",
Vary: "Accept",
},
});
},
};
}

0 comments on commit f20756a

Please sign in to comment.