From 24f1e7de92d609e0a9ee9df1e3dfa0d2f7c8f682 Mon Sep 17 00:00:00 2001 From: Isaac Adewumi Date: Wed, 24 Apr 2024 17:48:32 +0100 Subject: [PATCH] add framework support for Hono (#539) ## Summary Adds support for [Hono](https://hono.dev) framework. Currently for `hono.test.ts`, - 16/21 tests failing (76%) - 3/21 tests passing (14%) In comparison, other frameworks (e.g., Koa, Fastify) have: - 6/21 tests failing (29%) - 13/21 tests passing (62%) ## Checklist - [ ] Added a [docs PR](https://github.com/inngest/website) that references this PR - [x] Added unit/integration tests - [x] Added changesets if applicable ## Related - INN- --------- Co-authored-by: Jack Williams Co-authored-by: Jack Williams <1736957+jpwilliams@users.noreply.github.com> --- .changeset/chatty-ducks-mate.md | 5 ++ examples/framework-hono/.gitignore | 10 +++ examples/framework-hono/README.md | 36 +++++++++ examples/framework-hono/package.json | 13 ++++ examples/framework-hono/src/index.ts | 16 ++++ examples/framework-hono/src/inngest/client.ts | 4 + .../framework-hono/src/inngest/helloWorld.ts | 11 +++ examples/framework-hono/src/inngest/index.ts | 5 ++ examples/framework-hono/src/inngest/types.ts | 10 +++ examples/framework-hono/tsconfig.json | 16 ++++ examples/framework-hono/wrangler.toml | 21 ++++++ packages/inngest/.eslintrc.js | 1 + packages/inngest/package.json | 6 ++ packages/inngest/scripts/checkDependencies.ts | 1 + packages/inngest/src/hono.test.ts | 73 +++++++++++++++++++ packages/inngest/src/hono.ts | 48 ++++++++++++ packages/inngest/src/types.ts | 3 +- pnpm-lock.yaml | 10 ++- 18 files changed, 287 insertions(+), 2 deletions(-) create mode 100644 .changeset/chatty-ducks-mate.md create mode 100644 examples/framework-hono/.gitignore create mode 100644 examples/framework-hono/README.md create mode 100644 examples/framework-hono/package.json create mode 100644 examples/framework-hono/src/index.ts create mode 100644 examples/framework-hono/src/inngest/client.ts create mode 100644 examples/framework-hono/src/inngest/helloWorld.ts create mode 100644 examples/framework-hono/src/inngest/index.ts create mode 100644 examples/framework-hono/src/inngest/types.ts create mode 100644 examples/framework-hono/tsconfig.json create mode 100644 examples/framework-hono/wrangler.toml create mode 100644 packages/inngest/src/hono.test.ts create mode 100644 packages/inngest/src/hono.ts diff --git a/.changeset/chatty-ducks-mate.md b/.changeset/chatty-ducks-mate.md new file mode 100644 index 000000000..25caf4fc1 --- /dev/null +++ b/.changeset/chatty-ducks-mate.md @@ -0,0 +1,5 @@ +--- +"inngest": minor +--- + +Added framework support for Hono diff --git a/examples/framework-hono/.gitignore b/examples/framework-hono/.gitignore new file mode 100644 index 000000000..3c0be6ea7 --- /dev/null +++ b/examples/framework-hono/.gitignore @@ -0,0 +1,10 @@ +node_modules +dist +.wrangler +.dev.vars + +# Change them to your taste: +package-lock.json +yarn.lock +pnpm-lock.yaml +bun.lockb \ No newline at end of file diff --git a/examples/framework-hono/README.md b/examples/framework-hono/README.md new file mode 100644 index 000000000..f490d1ba1 --- /dev/null +++ b/examples/framework-hono/README.md @@ -0,0 +1,36 @@ +# Inngest Hono Template + +This is a [Hono](https://hono.dev/) v4 project targetting Cloudflare Workers. It is a reference on how to send and receive events with Inngest and Hono. + +## Getting Started + +Use [`create-next-app`](https://www.npmjs.com/package/create-next-app) with [npm](https://docs.npmjs.com/cli/init), [Yarn](https://yarnpkg.com/lang/en/docs/cli/create/), or [pnpm](https://pnpm.io) to bootstrap the example: + +```bash +npx create-next-app --example https://github.com/inngest/inngest-js/tree/main/examples/framework-hono inngest-hono +``` + +```bash +yarn create next-app --example https://github.com/inngest/inngest-js/tree/main/examples/framework-hono inngest-hono +``` + +```bash +pnpm create next-app --example https://github.com/inngest/inngest-js/tree/main/examples/framework-hono inngest-hono +``` + +### Run the app locally + +```sh +npm install +npm run dev +``` + +Open http://localhost:3000/api/inngest with your browser to see the result. + +- [Inngest functions](https://www.inngest.com/docs/functions) are available at `src/inngest/`. + + +## Learn More + +- [Inngest Documentation](https://www.inngest.com/docs) - learn about the Inngest SDK, functions, and events +- [Hono Documentation](https://hono.dev/top) - learn about Hono features and API diff --git a/examples/framework-hono/package.json b/examples/framework-hono/package.json new file mode 100644 index 000000000..e00af86ee --- /dev/null +++ b/examples/framework-hono/package.json @@ -0,0 +1,13 @@ +{ + "scripts": { + "dev": "wrangler dev --port 3000 src/index.ts", + "deploy": "wrangler deploy --minify src/index.ts" + }, + "dependencies": { + "hono": "^4.2.7" + }, + "devDependencies": { + "@cloudflare/workers-types": "^4.20240403.0", + "wrangler": "^3.47.0" + } +} diff --git a/examples/framework-hono/src/index.ts b/examples/framework-hono/src/index.ts new file mode 100644 index 000000000..731da1750 --- /dev/null +++ b/examples/framework-hono/src/index.ts @@ -0,0 +1,16 @@ +import { Hono } from "hono"; +import { serve } from "inngest/hono"; +import { functions, inngest } from "./inngest"; + +const app = new Hono(); + +app.on( + ["GET", "PUT", "POST"], + "/api/inngest", + serve({ + client: inngest, + functions, + }) +); + +export default app; diff --git a/examples/framework-hono/src/inngest/client.ts b/examples/framework-hono/src/inngest/client.ts new file mode 100644 index 000000000..a57be4d4f --- /dev/null +++ b/examples/framework-hono/src/inngest/client.ts @@ -0,0 +1,4 @@ +import { Inngest } from "inngest"; +import { schemas } from "./types"; + +export const inngest = new Inngest({ id: "my-bun-app", schemas }); diff --git a/examples/framework-hono/src/inngest/helloWorld.ts b/examples/framework-hono/src/inngest/helloWorld.ts new file mode 100644 index 000000000..87398c5f8 --- /dev/null +++ b/examples/framework-hono/src/inngest/helloWorld.ts @@ -0,0 +1,11 @@ +import { inngest } from "./client"; + +export const helloWorld = inngest.createFunction( + { id: "hello-world" }, + { event: "demo/event.sent" }, + async ({ event, step }) => { + return { + message: `Hello ${event.name}!`, + }; + } +); diff --git a/examples/framework-hono/src/inngest/index.ts b/examples/framework-hono/src/inngest/index.ts new file mode 100644 index 000000000..24ff4c9d2 --- /dev/null +++ b/examples/framework-hono/src/inngest/index.ts @@ -0,0 +1,5 @@ +import { helloWorld } from "./helloWorld"; + +export const functions = [helloWorld]; + +export { inngest } from "./client"; diff --git a/examples/framework-hono/src/inngest/types.ts b/examples/framework-hono/src/inngest/types.ts new file mode 100644 index 000000000..bb570b2c0 --- /dev/null +++ b/examples/framework-hono/src/inngest/types.ts @@ -0,0 +1,10 @@ +import { EventSchemas } from "inngest"; + +type DemoEventSent = { + name: "demo/event.sent"; + data: { + message: string; + }; +}; + +export const schemas = new EventSchemas().fromUnion(); diff --git a/examples/framework-hono/tsconfig.json b/examples/framework-hono/tsconfig.json new file mode 100644 index 000000000..33a96fd08 --- /dev/null +++ b/examples/framework-hono/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "lib": [ + "ESNext" + ], + "types": [ + "@cloudflare/workers-types" + ], + "jsx": "react-jsx", + "jsxImportSource": "hono/jsx" + }, +} \ No newline at end of file diff --git a/examples/framework-hono/wrangler.toml b/examples/framework-hono/wrangler.toml new file mode 100644 index 000000000..4e3ed97fc --- /dev/null +++ b/examples/framework-hono/wrangler.toml @@ -0,0 +1,21 @@ +name = "framework-hono" +compatibility_date = "2023-12-01" + +# [vars] +# MY_VAR = "my-variable" + +# [[kv_namespaces]] +# binding = "MY_KV_NAMESPACE" +# id = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + +# [[r2_buckets]] +# binding = "MY_BUCKET" +# bucket_name = "my-bucket" + +# [[d1_databases]] +# binding = "DB" +# database_name = "my-database" +# database_id = "" + +# [ai] +# binding = "AI" \ No newline at end of file diff --git a/packages/inngest/.eslintrc.js b/packages/inngest/.eslintrc.js index e7959f1b5..0598dbe74 100644 --- a/packages/inngest/.eslintrc.js +++ b/packages/inngest/.eslintrc.js @@ -42,6 +42,7 @@ module.exports = { "src/fastify.ts", "src/h3.ts", "src/koa.ts", + "src/hono.ts", "src/lambda.ts", "src/next.ts", "src/nuxt.ts", diff --git a/packages/inngest/package.json b/packages/inngest/package.json index 57e9702e2..3f075d692 100644 --- a/packages/inngest/package.json +++ b/packages/inngest/package.json @@ -115,6 +115,11 @@ "import": "./deno/fresh.js", "types": "./deno/fresh.d.ts" }, + "./hono": { + "require": "./hono.js", + "import": "./hono.js", + "types": "./hono.d.ts" + }, "./api/*": "./api/*.js", "./components/*": "./components/*.js", "./deno/*": "./deno/*.js", @@ -210,6 +215,7 @@ "fastify": "^4.21.0", "genversion": "^3.1.1", "glob": "^10.3.10", + "hono": "^4.2.3", "inquirer": "^9.2.10", "jest": "^29.3.1", "jest-fetch-mock": "^3.0.3", diff --git a/packages/inngest/scripts/checkDependencies.ts b/packages/inngest/scripts/checkDependencies.ts index a18015b8d..2e385446e 100644 --- a/packages/inngest/scripts/checkDependencies.ts +++ b/packages/inngest/scripts/checkDependencies.ts @@ -277,6 +277,7 @@ checkDependencies("tsconfig.build.json", [ "src/fastify.ts", "src/h3.ts", "src/koa.ts", + "src/hono.ts", "src/lambda.ts", "src/next.ts", "src/nuxt.ts", diff --git a/packages/inngest/src/hono.test.ts b/packages/inngest/src/hono.test.ts new file mode 100644 index 000000000..a5083ff60 --- /dev/null +++ b/packages/inngest/src/hono.test.ts @@ -0,0 +1,73 @@ +import * as HonoHandler from "@local/hono"; +import fetch, { Headers, Response } from "cross-fetch"; +import { testFramework } from "./test/helpers"; + +const originalFetch = globalThis.fetch; +const originalResponse = globalThis.Response; +const originalHeaders = globalThis.Headers; + +testFramework("Hono", HonoHandler, { + /** + * Make sure this stuff is available for all polyfilled Node environments. + */ + lifecycleChanges: () => { + beforeEach(() => { + jest.resetModules(); + Object.defineProperties(globalThis, { + /** + * Fake a global `fetch` value, which is available as as a Web Standard + * API. + */ + fetch: { value: fetch, configurable: true }, + /** + * Fake a global `Response` class, which is used to create new responses + * for the handler. + */ + Response: { value: Response, configurable: true }, + /** + * Fake a global `Headers` class, which is used to create new Headers + * objects during response building. + */ + Headers: { value: Headers, configurable: true }, + }); + }); + afterEach(() => { + /** + * Reset all changes made to the global scope + */ + Object.defineProperties(globalThis, { + fetch: { value: originalFetch, configurable: true }, + Response: { value: originalResponse, configurable: true }, + Headers: { value: originalHeaders, configurable: true }, + }); + }); + }, + transformReq: (req) => { + const c = { + req: { + // in practice, this is an absolute URL + url: new URL(`https://${req.headers["host"]}${req.url}`).href, + query: (key: string) => + new URLSearchParams(req.url.split("?")[1] || "").get(key), + header: (key: string) => req.headers[key] as string, + method: req.method, + json: () => Promise.resolve(req.body), + }, + body: (data: BodyInit, init: ResponseInit) => new Response(data, init), + }; + return [c]; + }, + transformRes: async (_args, ret: Response) => { + const headers: Record = {}; + + ret.headers.forEach((v, k) => { + headers[k] = v; + }); + + return { + status: ret.status, + body: await ret.text(), + headers, + }; + }, +}); diff --git a/packages/inngest/src/hono.ts b/packages/inngest/src/hono.ts new file mode 100644 index 000000000..cd17101a9 --- /dev/null +++ b/packages/inngest/src/hono.ts @@ -0,0 +1,48 @@ +import { type Context } from "hono"; +import { + InngestCommHandler, + type ServeHandlerOptions, +} from "./components/InngestCommHandler"; +import { type SupportedFrameworkName } from "./types"; + +export const frameworkName: SupportedFrameworkName = "hono"; + +/** + * Using Hono, serve and register any declared functions with Inngest, + * making them available to be triggered by events. + * + * @example + * ```ts + * const handler = serve({ + * client: inngest, + * functions + * }); + * + * app.use('/api/inngest', async (c) => { + * return handler(c); + * }); + * ``` + * + * @public + */ +export const serve = (options: ServeHandlerOptions) => { + const handler = new InngestCommHandler({ + fetch: fetch.bind(globalThis), + frameworkName, + ...options, + handler: (c: Context) => { + return { + transformResponse: ({ headers, status, body }) => { + return c.body(body, { headers, status }); + }, + url: () => new URL(c.req.url, c.req.header("host")), + queryString: (key) => c.req.query(key), + headers: (key) => c.req.header(key), + method: () => c.req.method, + body: () => c.req.json(), + }; + }, + }); + + return handler.createHandler(); +}; diff --git a/packages/inngest/src/types.ts b/packages/inngest/src/types.ts index 20d8db3e8..09d51053a 100644 --- a/packages/inngest/src/types.ts +++ b/packages/inngest/src/types.ts @@ -1102,7 +1102,8 @@ export type SupportedFrameworkName = | "deno/fresh" | "sveltekit" | "fastify" - | "koa"; + | "koa" + | "hono"; /** * A set of options that can be passed to any step to configure it. diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index c27eb41cc..6702df56d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -44,7 +44,7 @@ importers: version: 29.5.0(@types/node@18.16.16) ts-jest: specifier: ^29.1.0 - version: 29.1.0(@babel/core@7.21.3)(jest@29.5.0)(typescript@5.4.2) + version: 29.1.0(@babel/core@7.23.6)(jest@29.5.0)(typescript@5.4.2) packages/eslint-plugin-internal: dependencies: @@ -181,6 +181,9 @@ importers: glob: specifier: ^10.3.10 version: 10.3.10 + hono: + specifier: ^4.2.3 + version: 4.2.3 inquirer: specifier: ^9.2.10 version: 9.2.10 @@ -6037,6 +6040,11 @@ packages: space-separated-tokens: 2.0.2 dev: true + /hono@4.2.3: + resolution: {integrity: sha512-yZDnPOp/XzjIB7KUWaOxwLSywnhxMvAKth8hfKhWQiWXeZhBfC6GlFnEst/FOOgn7rSWjShhQPS89PLEuHxq3Q==} + engines: {node: '>=16.0.0'} + dev: true + /hosted-git-info@2.8.9: resolution: {integrity: sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==} dev: true