diff --git a/.changeset/eleven-islands-jump.md b/.changeset/eleven-islands-jump.md new file mode 100644 index 00000000..622b3632 --- /dev/null +++ b/.changeset/eleven-islands-jump.md @@ -0,0 +1,5 @@ +--- +'@hono/tsyringe': major +--- + +add tsyringe middleware support diff --git a/.github/workflows/ci-tsyringe.yml b/.github/workflows/ci-tsyringe.yml new file mode 100644 index 00000000..76ff993a --- /dev/null +++ b/.github/workflows/ci-tsyringe.yml @@ -0,0 +1,25 @@ +name: ci-tsyringe +on: + push: + branches: [main] + paths: + - 'packages/tsyringe/**' + pull_request: + branches: ['*'] + paths: + - 'packages/tsyringe/**' + +jobs: + ci: + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./packages/tsyringe + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20.x + - run: yarn install --frozen-lockfile + - run: yarn build + - run: yarn test diff --git a/packages/tsyringe/README.md b/packages/tsyringe/README.md new file mode 100644 index 00000000..1c8fcaf6 --- /dev/null +++ b/packages/tsyringe/README.md @@ -0,0 +1,57 @@ +# tsyringe middleware for Hono + +The [tsyringe](https://github.com/microsoft/tsyringe) middleware provides a way to use dependency injection in [Hono](https://hono.dev/). + +## Usage + +```ts +import "reflect-metadata" // tsyringe requires reflect-metadata or polyfill +import { container, inject, injectable } from 'tsyringe' +import { tsyringe } from '@hono/tsyringe' +import { Hono } from 'hono' + +@injectable() +class Hello { + constructor(@inject('name') private name: string) {} + + greet() { + return `Hello, ${this.name}!` + } +} + +const app = new Hono() + +app.use('*', tsyringe((container) => { + container.register('name', { useValue: 'world' }) +})) + +app.get('/', (c) => { + const hello = container.resolve(Hello) + return c.text(hello.greet()) +}) + +export default app +``` + +### With providers + +```ts +const app = new Hono() + +app.use('/tenant/:name/*', async (c, next) => { + await tsyringe((container) => { + // Allowing to inject `c.var` or `c.req.param` in the providers + const tenantName = c.req.param('name') + + container.register(Config, { useFactory: () => new Config(tenantName) }) + })(c, next) +}) +``` + +## Author + +Aotokitsuruya + +## License + +MIT diff --git a/packages/tsyringe/package.json b/packages/tsyringe/package.json new file mode 100644 index 00000000..dc05efe6 --- /dev/null +++ b/packages/tsyringe/package.json @@ -0,0 +1,51 @@ +{ + "name": "@hono/tsyringe", + "version": "0.1.0", + "description": "The tsyringe dependency injection middleware for Hono", + "type": "module", + "module": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], + "scripts": { + "test": "vitest --run", + "build": "tsup ./src/index.ts --format esm,cjs --dts", + "publint": "publint", + "release": "yarn build && yarn test && yarn publint && yarn publish" + }, + "exports": { + ".": { + "import": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "require": { + "types": "./dist/index.d.cts", + "default": "./dist/index.cjs" + } + } + }, + "license": "MIT", + "publishConfig": { + "registry": "https://registry.npmjs.org", + "access": "public" + }, + "repository": { + "type": "git", + "url": "https://github.com/honojs/middleware.git" + }, + "homepage": "https://github.com/honojs/middleware", + "peerDependencies": { + "hono": ">=4.*", + "tsyringe": ">=4.*" + }, + "devDependencies": { + "hono": "^4.4.12", + "prettier": "^3.3.3", + "reflect-metadata": "^0.2.2", + "tsup": "^8.1.0", + "tsyringe": "^4.8.0", + "vitest": "^1.6.0" + } +} diff --git a/packages/tsyringe/src/index.test.ts b/packages/tsyringe/src/index.test.ts new file mode 100644 index 00000000..43f7e828 --- /dev/null +++ b/packages/tsyringe/src/index.test.ts @@ -0,0 +1,57 @@ +import 'reflect-metadata' +import { injectable, inject } from 'tsyringe' +import { Hono } from 'hono' +import { tsyringe } from '../src' + +class Config { + constructor(public readonly tenantName: string) {} +} + +@injectable() +class TenantService { + constructor(@inject(Config) public readonly config: Config) {} + + get message() { + return `Hello, ${this.config.tenantName}!` + } +} + +describe('tsyringe middleware', () => { + const app = new Hono() + + app.use( + '/hello/*', + tsyringe((container) => container.register('foo', { useValue: 'Hello!' })) + ) + app.get('/hello/foo', (c) => { + const message = c.var.resolve('foo') + return c.text(message) + }) + + app.use('/tenant/:name/*', async (c, next) => { + await tsyringe((container) => { + const tenantName = c.req.param('name') + + container.register(Config, { useFactory: () => new Config(tenantName) }) + })(c, next) + }) + + app.get('/tenant/:name/message', (c) => { + const tenantService = c.var.resolve(TenantService) + return c.text(tenantService.message) + }) + + it('Should be hello message', async () => { + const res = await app.request('http://localhost/hello/foo') + expect(res).not.toBeNull() + expect(res.status).toBe(200) + expect(await res.text()).toBe('Hello!') + }) + + it('Should be tenant message', async () => { + const res = await app.request('http://localhost/tenant/foo/message') + expect(res).not.toBeNull() + expect(res.status).toBe(200) + expect(await res.text()).toBe('Hello, foo!') + }) +}) diff --git a/packages/tsyringe/src/index.ts b/packages/tsyringe/src/index.ts new file mode 100644 index 00000000..1a0c25ec --- /dev/null +++ b/packages/tsyringe/src/index.ts @@ -0,0 +1,19 @@ +import { container, DependencyContainer, InjectionToken } from 'tsyringe' +import type { Context, MiddlewareHandler } from 'hono' +import { createMiddleware } from 'hono/factory' + +declare module 'hono' { + interface ContextVariableMap { + resolve: (token: InjectionToken) => T + } +} + +export type Provider = (container: DependencyContainer) => void +export const tsyringe = (...providers: Provider[]): MiddlewareHandler => { + return createMiddleware(async (c, next) => { + const childContainer = container.createChildContainer() + providers.forEach((provider) => provider(childContainer)) + c.set('resolve', (token: InjectionToken) => childContainer.resolve(token)) + await next() + }) +} diff --git a/packages/tsyringe/tsconfig.json b/packages/tsyringe/tsconfig.json new file mode 100644 index 00000000..3cbc5a07 --- /dev/null +++ b/packages/tsyringe/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist", + "experimentalDecorators": true, + "emitDecoratorMetadata": true + }, + "include": [ + "src/**/*.ts" + ], +} diff --git a/packages/tsyringe/vitest.config.ts b/packages/tsyringe/vitest.config.ts new file mode 100644 index 00000000..17b54e48 --- /dev/null +++ b/packages/tsyringe/vitest.config.ts @@ -0,0 +1,8 @@ +/// +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + globals: true, + }, +}) diff --git a/yarn.lock b/yarn.lock index 8b66811c..3c6a33b2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2863,6 +2863,22 @@ __metadata: languageName: unknown linkType: soft +"@hono/tsyringe@workspace:packages/tsyringe": + version: 0.0.0-use.local + resolution: "@hono/tsyringe@workspace:packages/tsyringe" + dependencies: + hono: "npm:^4.4.12" + prettier: "npm:^3.3.3" + reflect-metadata: "npm:^0.2.2" + tsup: "npm:^8.1.0" + tsyringe: "npm:^4.8.0" + vitest: "npm:^1.6.0" + peerDependencies: + hono: ">=4.*" + tsyringe: ">=4.*" + languageName: unknown + linkType: soft + "@hono/typebox-validator@workspace:packages/typebox-validator": version: 0.0.0-use.local resolution: "@hono/typebox-validator@workspace:packages/typebox-validator" @@ -16823,6 +16839,15 @@ __metadata: languageName: node linkType: hard +"prettier@npm:^3.3.3": + version: 3.3.3 + resolution: "prettier@npm:3.3.3" + bin: + prettier: bin/prettier.cjs + checksum: b85828b08e7505716324e4245549b9205c0cacb25342a030ba8885aba2039a115dbcf75a0b7ca3b37bc9d101ee61fab8113fc69ca3359f2a226f1ecc07ad2e26 + languageName: node + linkType: hard + "pretty-format@npm:^28.0.0, pretty-format@npm:^28.1.3": version: 28.1.3 resolution: "pretty-format@npm:28.1.3" @@ -19522,7 +19547,7 @@ __metadata: languageName: node linkType: hard -"tslib@npm:^1.8.1, tslib@npm:^1.9.0": +"tslib@npm:^1.8.1, tslib@npm:^1.9.0, tslib@npm:^1.9.3": version: 1.14.1 resolution: "tslib@npm:1.14.1" checksum: 69ae09c49eea644bc5ebe1bca4fa4cc2c82b7b3e02f43b84bd891504edf66dbc6b2ec0eef31a957042de2269139e4acff911e6d186a258fb14069cd7f6febce2 @@ -19830,6 +19855,15 @@ __metadata: languageName: node linkType: hard +"tsyringe@npm:^4.8.0": + version: 4.8.0 + resolution: "tsyringe@npm:4.8.0" + dependencies: + tslib: "npm:^1.9.3" + checksum: e13810e8ff39c4093acd0649bc5db3c164825827631e1522cd9d5ca8694a018447fa1c24f059ea54e93b1020767b1131b9dc9ce598dabfc9aa41c11544bbfe19 + languageName: node + linkType: hard + "tty-table@npm:^4.1.5": version: 4.2.3 resolution: "tty-table@npm:4.2.3"