Skip to content

Commit e865d6e

Browse files
committed
Merge remote-tracking branch 'express/main'
2 parents 649de50 + 70f7604 commit e865d6e

16 files changed

Lines changed: 1009 additions & 46 deletions

File tree

.github/workflows/build.yaml

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -379,17 +379,21 @@ jobs:
379379
- [`npm:@fedify/fedify@${{ steps.versioning.outputs.short_version }}`][2]
380380
- [`jsr:@fedify/amqp@${{ steps.versioning.outputs.version }}`][3]
381381
- [`npm:@fedify/amqp@${{ steps.versioning.outputs.short_version }}`][4]
382-
- [`jsr:@fedify/postgres@${{ steps.versioning.outputs.version }}`][5]
383-
- [`npm:@fedify/postgres@${{ steps.versioning.outputs.short_version }}`][6]
384-
- [`jsr:@fedify/cli@${{ steps.versioning.outputs.version }}`][7]
382+
- [`jsr:@fedify/express@${{ steps.versioning.outputs.version }}`][5]
383+
- [`npm:@fedify/express@${{ steps.versioning.outputs.short_version }}`][6]
384+
- [`jsr:@fedify/postgres@${{ steps.versioning.outputs.version }}`][7]
385+
- [`npm:@fedify/postgres@${{ steps.versioning.outputs.short_version }}`][8]
386+
- [`jsr:@fedify/cli@${{ steps.versioning.outputs.version }}`][9]
385387
386388
[1]: https://jsr.io/@fedify/fedify@${{ steps.versioning.outputs.version }}
387389
[2]: https://www.npmjs.com/package/@fedify/fedify/v/${{ steps.versioning.outputs.short_version }}
388390
[3]: https://jsr.io/@fedify/amqp@${{ steps.versioning.outputs.version }}
389391
[4]: https://www.npmjs.com/package/@fedify/amqp/v/${{ steps.versioning.outputs.short_version }}
390-
[5]: https://jsr.io/@fedify/postgres@${{ steps.versioning.outputs.version }}
391-
[6]: https://www.npmjs.com/package/@fedify/postgres/v/${{ steps.versioning.outputs.short_version }}
392-
[7]: https://jsr.io/@fedify/cli@${{ steps.versioning.outputs.version }}
392+
[5]: https://jsr.io/@fedify/express@${{ steps.versioning.outputs.version }}
393+
[6]: https://www.npmjs.com/package/@fedify/express/v/${{ steps.versioning.outputs.short_version }}
394+
[7]: https://jsr.io/@fedify/postgres@${{ steps.versioning.outputs.version }}
395+
[8]: https://www.npmjs.com/package/@fedify/postgres/v/${{ steps.versioning.outputs.short_version }}
396+
[9]: https://jsr.io/@fedify/cli@${{ steps.versioning.outputs.version }}
393397
pr-number: ${{ github.event.pull_request.number }}
394398
comment-tag: publish
395399

deno.json

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
"./fedify",
44
"./cli",
55
"./amqp",
6+
"./express",
67
"./postgres",
78
"./examples/blog",
89
"./examples/cloudflare-workers",
@@ -35,19 +36,35 @@
3536
"nodeModulesDir": "none",
3637
"tasks": {
3738
"codegen": "deno task -f @fedify/cli codegen",
38-
"check": {
39-
"command": "deno task -f @fedify/fedify check && deno task -f @fedify/cli check && deno task -f @fedify/blog check && deno task -f @fedify/hono-sample check",
39+
"check-versions": "deno run --allow-read --allow-write scripts/check_versions.ts",
40+
"check-all": {
41+
"command": "deno task --recursive check",
4042
"dependencies": [
41-
"check-versions"
43+
"check-versions",
44+
"codegen"
45+
]
46+
},
47+
"test": {
48+
"command": "deno test --check --doc --allow-all --unstable-kv --trace-leaks --parallel",
49+
"dependencies": [
50+
"codegen"
51+
]
52+
},
53+
"test:node": {
54+
"command": "pnpm run --recursive --filter '!{docs}' test",
55+
"dependencies": [
56+
"codegen"
57+
]
58+
},
59+
"test:bun": {
60+
"command": "pnpm run --recursive --filter '!{docs}' test:bun",
61+
"dependencies": [
62+
"codegen"
4263
]
4364
},
44-
"check-versions": "deno run --allow-read --allow-write scripts/check_versions.ts",
45-
"test": "deno test --check --doc --allow-all --unstable-kv --trace-leaks --parallel",
46-
"test:node": "pnpm run --recursive --filter '!{docs}' test",
47-
"test:bun": "pnpm run --recursive --filter '!{docs}' test:bun",
4865
"test-all": {
4966
"dependencies": [
50-
"check",
67+
"check-all",
5168
"test",
5269
"test:node",
5370
"test:bun"
@@ -58,7 +75,7 @@
5875
"hooks:install": "deno run --allow-read=deno.json,.git/hooks/ --allow-write=.git/hooks/ jsr:@hongminhee/deno-task-hooks",
5976
"hooks:pre-commit": {
6077
"dependencies": [
61-
"check"
78+
"check-all"
6279
]
6380
}
6481
}

docs/manual/integration.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,8 @@ Express
7676
The [@fedify/express] package provides a middleware to integrate Fedify with
7777
Express:
7878

79-
~~~~ typescript
79+
~~~~ typescript twoslash
80+
// @noErrors: 2345
8081
import express from "express";
8182
import { integrateFederation } from "@fedify/express";
8283
import { createFederation } from "@fedify/fedify";

docs/package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
"@cloudflare/workers-types": "catalog:",
55
"@deno/kv": "^0.8.4",
66
"@fedify/amqp": "workspace:",
7+
"@fedify/express": "workspace:",
78
"@fedify/fedify": "workspace:",
89
"@fedify/postgres": "workspace:",
910
"@fedify/redis": "^0.4.0",
@@ -19,9 +20,11 @@
1920
"@types/amqplib": "catalog:",
2021
"@types/better-sqlite3": "^7.6.12",
2122
"@types/bun": "^1.1.14",
22-
"@types/node": "^22.15.21",
23+
"@types/express": "catalog:",
24+
"@types/node": "catalog:",
2325
"amqplib": "catalog:",
2426
"dayjs": "^1.11.13",
27+
"express": "catalog:",
2528
"hono": "^4.6.14",
2629
"ioredis": "^5.4.2",
2730
"markdown-it-abbr": "^2.0.0",

examples/express/federation.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { MemoryKvStore, Note, Person, createFederation } from "@fedify/fedify";
2+
3+
export const federation = createFederation<void>({
4+
kv: new MemoryKvStore(),
5+
});
6+
7+
federation.setActorDispatcher("/users/{handle}", async (ctx, handle) => {
8+
return new Person({
9+
id: ctx.getActorUri(handle),
10+
preferredUsername: handle,
11+
});
12+
});
13+
14+
federation.setObjectDispatcher(
15+
Note,
16+
"/users/{handle}/{id}",
17+
async (ctx, values) => {
18+
return new Note({
19+
id: ctx.getObjectUri(Note, values),
20+
name: values.id,
21+
});
22+
},
23+
);

examples/express/index.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import { integrateFederation } from "@fedify/express";
2+
import express from "express";
3+
import { federation } from "./federation.ts";
4+
5+
export const app = express();
6+
7+
app.set("trust proxy", true);
8+
9+
app.use(integrateFederation(federation, () => undefined));
10+
11+
app.get("/users/:handle", (req, res) => {
12+
res.type("html").send(`<p>Hello, ${req.params.handle}!</p>`);
13+
});
14+
15+
app.listen(3000, () => {
16+
console.log("Listening on http://localhost:3000");
17+
});

express/README.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<!-- deno-fmt-ignore-file -->
2+
3+
@fedify/express: Integrate Fedify with Express
4+
==============================================
5+
6+
[![npm][npm badge]][npm]
7+
[![Matrix][Matrix badge]][Matrix]
8+
[![Follow @fedify@hollo.social][@fedify@hollo.social badge]][@fedify@hollo.social]
9+
10+
This package provides a simple way to integrate [Fedify] with [Express].
11+
12+
The integration code looks like this:
13+
14+
~~~~ typescript
15+
import express from "express";
16+
import { integrateFederation } from "@fedify/express";
17+
import { federation } from "./federation.ts"; // Your `Federation` instance
18+
19+
export const app = express();
20+
21+
app.set("trust proxy", true);
22+
23+
app.use(integrateFederation(federation, (req) => "context data goes here"));
24+
~~~~
25+
26+
[npm]: https://www.npmjs.com/package/@fedify/express
27+
[npm badge]: https://img.shields.io/npm/v/@fedify/express?logo=npm
28+
[Matrix]: https://matrix.to/#/#fedify:matrix.org
29+
[Matrix badge]: https://img.shields.io/matrix/fedify%3Amatrix.org
30+
[@fedify@hollo.social badge]: https://fedi-badge.deno.dev/@fedify@hollo.social/followers.svg
31+
[@fedify@hollo.social]: https://hollo.social/@fedify
32+
[Fedify]: https://fedify.dev/
33+
[Express]: https://expressjs.com/

express/deno.json

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"name": "@fedify/express",
3+
"version": "1.8.0",
4+
"license": "MIT",
5+
"exports": {
6+
".": "./index.ts"
7+
},
8+
"imports": {
9+
"express": "npm:express@^4.0.0"
10+
},
11+
"nodeModulesDir": "none",
12+
"unstable": [
13+
"temporal"
14+
],
15+
"exclude": [
16+
"dist",
17+
"node_modules"
18+
],
19+
"tasks": {
20+
"check": "deno fmt --check && deno lint && deno check *.ts"
21+
}
22+
}

express/index.ts

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import type { Federation } from "@fedify/fedify";
2+
import type {
3+
NextFunction,
4+
Request as ERequest,
5+
Response as EResponse,
6+
} from "express";
7+
import { Buffer } from "node:buffer";
8+
import { Readable } from "node:stream";
9+
10+
type Middleware = (req: ERequest, res: EResponse, next: NextFunction) => void;
11+
12+
export type ContextDataFactory<TContextData> = (
13+
req: ERequest,
14+
) => TContextData | Promise<TContextData>;
15+
16+
export function integrateFederation<TContextData>(
17+
federation: Federation<TContextData>,
18+
contextDataFactory: ContextDataFactory<TContextData>,
19+
): Middleware {
20+
return (req, res, next) => {
21+
const request = fromERequest(req);
22+
const contextData = contextDataFactory(req);
23+
const contextDataPromise = contextData instanceof Promise
24+
? contextData
25+
: Promise.resolve(contextData);
26+
contextDataPromise.then(async (contextData) => {
27+
let notFound = false;
28+
let notAcceptable = false;
29+
const response = await federation.fetch(request, {
30+
contextData,
31+
onNotFound: () => {
32+
// If the `federation` object finds a request not responsible for it
33+
// (i.e., not a federation-related request), it will call the `next`
34+
// function provided by the Express framework to continue the request
35+
// handling by the Express:
36+
notFound = true;
37+
next();
38+
return new Response("Not found", { status: 404 }); // unused
39+
},
40+
onNotAcceptable: () => {
41+
// Similar to `onNotFound`, but slightly more tricky.
42+
// When the `federation` object finds a request not acceptable
43+
// type-wise (i.e., a user-agent doesn't want JSON-LD), it will call
44+
// the `next` function provided by the Express framework to continue
45+
// if any route is matched, and otherwise, it will return a 406 Not
46+
// Acceptable response:
47+
notAcceptable = true;
48+
next();
49+
return new Response("Not acceptable", {
50+
status: 406,
51+
headers: {
52+
"Content-Type": "text/plain",
53+
Vary: "Accept",
54+
},
55+
});
56+
},
57+
});
58+
if (notFound || (notAcceptable && req.route != null)) return;
59+
await setEResponse(res, response);
60+
// Prevent the Express framework from sending the response again:
61+
res.end();
62+
res.status = () => res;
63+
res.send = () => res;
64+
res.end = () => res;
65+
res.json = () => res;
66+
res.removeHeader = () => res;
67+
res.setHeader = () => res;
68+
});
69+
};
70+
}
71+
72+
function fromERequest(req: ERequest): Request {
73+
const url = `${req.protocol}://${
74+
req.header("Host") ?? req.hostname
75+
}${req.url}`;
76+
const headers = new Headers();
77+
for (const [key, value] of Object.entries(req.headers)) {
78+
if (Array.isArray(value)) {
79+
for (const v of value) headers.append(key, v);
80+
} else if (typeof value === "string") {
81+
headers.append(key, value);
82+
}
83+
}
84+
return new Request(url, {
85+
method: req.method,
86+
headers,
87+
// @ts-ignore: duplex is not supported in Deno, but it is in Node.js
88+
duplex: "half",
89+
body: req.method === "GET" || req.method === "HEAD"
90+
? undefined
91+
: (Readable.toWeb(req)),
92+
});
93+
}
94+
95+
function setEResponse(res: EResponse, response: Response): Promise<void> {
96+
res.status(response.status);
97+
response.headers.forEach((value, key) => res.setHeader(key, value));
98+
if (response.body == null) return Promise.resolve();
99+
const body = response.body;
100+
return new Promise((resolve) => {
101+
const reader = body.getReader();
102+
reader.read().then(function read({ done, value }) {
103+
if (done) {
104+
reader.releaseLock();
105+
resolve();
106+
return;
107+
}
108+
res.write(Buffer.from(value));
109+
reader.read().then(read);
110+
});
111+
});
112+
}

express/package.json

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
{
2+
"name": "@fedify/express",
3+
"version": "1.8.0",
4+
"description": "Integrate Fedify with Express",
5+
"keywords": [
6+
"Fedify",
7+
"Express",
8+
"Express.js"
9+
],
10+
"author": {
11+
"name": "Hong Minhee",
12+
"email": "hong@minhee.org",
13+
"url": "https://hongminhee.org/"
14+
},
15+
"homepage": "https://fedify.dev/",
16+
"repository": {
17+
"type": "git",
18+
"url": "git+https://github.com/fedify-dev/fedify.git",
19+
"directory": "express"
20+
},
21+
"license": "MIT",
22+
"bugs": {
23+
"url": "https://github.com/fedify-dev/fedify/issues"
24+
},
25+
"funding": [
26+
"https://opencollective.com/fedify",
27+
"https://github.com/sponsors/dahlia"
28+
],
29+
"type": "module",
30+
"module": "./dist/index.js",
31+
"types": "./dist/index.d.ts",
32+
"exports": {
33+
".": {
34+
"import": {
35+
"types": "./dist/index.d.ts",
36+
"default": "./dist/index.js"
37+
}
38+
},
39+
"./package.json": "./package.json"
40+
},
41+
"files": [
42+
"dist/",
43+
"package.json"
44+
],
45+
"peerDependencies": {
46+
"@fedify/fedify": "workspace:",
47+
"express": "catalog:"
48+
},
49+
"devDependencies": {
50+
"@types/express": "catalog:",
51+
"@types/node": "catalog:",
52+
"tsdown": "catalog:",
53+
"typescript": "catalog:"
54+
},
55+
"scripts": {
56+
"build": "tsdown",
57+
"prepack": "tsdown",
58+
"prepublish": "tsdown"
59+
}
60+
}

0 commit comments

Comments
 (0)