From cdf4d5a0e70b49da38a56d65f6c0f7f225f84d80 Mon Sep 17 00:00:00 2001 From: Naily Date: Tue, 29 Oct 2024 22:29:17 +0800 Subject: [PATCH] feat: update ioc,backend package;add rpc protocoll package;add cache & config module;update docs --- .vscode/settings.json | 1 + docs/.vitepress/cache/deps/_metadata.json | 14 +-- docs/.vitepress/config.ts | 30 +++-- docs/en/index.md | 2 +- docs/index.md | 2 +- docs/ioc-guide/index.md | 39 +++++++ docs/restful-guide/index.md | 50 ++++++++ fixtures/backend/naily.config.ts | 3 + fixtures/backend/package.json | 22 ++++ fixtures/backend/src/main.ts | 9 ++ fixtures/backend/src/test.controller.ts | 20 ++++ fixtures/backend/src/test.filter.ts | 9 ++ fixtures/backend/tsconfig.json | 10 ++ fixtures/backend/tsup.config.ts | 11 ++ package.json | 3 + packages/backend/package.json | 8 +- packages/backend/src/backend-adapter.ts | 2 +- packages/backend/src/backend-container.ts | 47 +------- packages/backend/src/class-method-executor.ts | 80 +++++++++++++ packages/backend/src/constant.ts | 1 + packages/backend/src/decorators/index.ts | 1 + .../src/decorators/restful.decorator.ts | 74 ++++++++++++ packages/backend/src/handler-context.ts | 33 +++++- packages/backend/src/index.ts | 1 + packages/backend/src/node-adapter.ts | 42 +------ packages/backend/src/node/http-adapter.ts | 46 ++++++++ packages/backend/src/node/index.ts | 3 + packages/backend/src/node/node-bootstrap.ts | 41 +++++++ packages/backend/src/{ => node}/utils.ts | 9 +- packages/backend/src/types.ts | 4 + packages/backend/tsup.config.ts | 1 - packages/cache/package.json | 27 +++++ packages/cache/src/index.ts | 1 + packages/cache/tsconfig.json | 8 ++ packages/cache/tsup.config.ts | 11 ++ packages/config/package.json | 32 ++++++ packages/config/src/decorators/index.ts | 1 + .../config/src/decorators/value.decorator.ts | 18 +++ packages/config/src/index.ts | 3 + packages/config/src/plugin.ts | 57 ++++++++++ packages/config/src/types.ts | 43 +++++++ packages/config/tsconfig.json | 10 ++ packages/config/tsup.config.ts | 11 ++ packages/ioc/src/abstract-bootstrap.ts | 6 + .../ioc/src/constants/container-constant.ts | 5 - packages/ioc/src/container-protocol.ts | 4 +- packages/ioc/src/container.ts | 40 +++++-- .../ioc/src/decorators/inject.decorator.ts | 6 +- .../src/decorators/injectable.decorator.ts | 8 +- packages/ioc/src/index.ts | 2 +- packages/ioc/src/inject-wrapper.ts | 15 +++ packages/ioc/src/injectable-wrapper.ts | 49 +++++++- packages/ioc/src/plugin-protocol.ts | 5 + packages/jexl/package.json | 32 ++++++ packages/jexl/src/index.ts | 5 + packages/jexl/tsconfig.json | 10 ++ packages/jexl/tsup.config.ts | 11 ++ packages/rpc-protocol/package.json | 2 +- packages/rpc-protocol/src/index.ts | 8 +- packages/rpc/src/index.ts | 1 + ...context.ts => rpc-controller-container.ts} | 2 +- packages/rpc/src/rpc-handler.ts | 11 +- pnpm-lock.yaml | 107 ++++++++++++++++++ 63 files changed, 1021 insertions(+), 148 deletions(-) create mode 100644 docs/ioc-guide/index.md create mode 100644 docs/restful-guide/index.md create mode 100644 fixtures/backend/naily.config.ts create mode 100644 fixtures/backend/package.json create mode 100644 fixtures/backend/src/main.ts create mode 100644 fixtures/backend/src/test.controller.ts create mode 100644 fixtures/backend/src/test.filter.ts create mode 100644 fixtures/backend/tsconfig.json create mode 100644 fixtures/backend/tsup.config.ts create mode 100644 packages/backend/src/class-method-executor.ts create mode 100644 packages/backend/src/decorators/restful.decorator.ts create mode 100644 packages/backend/src/node/http-adapter.ts create mode 100644 packages/backend/src/node/index.ts create mode 100644 packages/backend/src/node/node-bootstrap.ts rename packages/backend/src/{ => node}/utils.ts (94%) create mode 100644 packages/backend/src/types.ts create mode 100644 packages/cache/package.json create mode 100644 packages/cache/src/index.ts create mode 100644 packages/cache/tsconfig.json create mode 100644 packages/cache/tsup.config.ts create mode 100644 packages/config/package.json create mode 100644 packages/config/src/decorators/index.ts create mode 100644 packages/config/src/decorators/value.decorator.ts create mode 100644 packages/config/src/index.ts create mode 100644 packages/config/src/plugin.ts create mode 100644 packages/config/src/types.ts create mode 100644 packages/config/tsconfig.json create mode 100644 packages/config/tsup.config.ts delete mode 100644 packages/ioc/src/constants/container-constant.ts create mode 100644 packages/ioc/src/inject-wrapper.ts create mode 100644 packages/ioc/src/plugin-protocol.ts create mode 100644 packages/jexl/package.json create mode 100644 packages/jexl/src/index.ts create mode 100644 packages/jexl/tsconfig.json create mode 100644 packages/jexl/tsup.config.ts rename packages/rpc/src/{rpc-handler-context.ts => rpc-controller-container.ts} (94%) diff --git a/.vscode/settings.json b/.vscode/settings.json index b2e2ee5..1b95286 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -9,6 +9,7 @@ "farmfe", "flexable", "Inversify", + "jexl", "linkcode", "Naily", "nailyjs", diff --git a/docs/.vitepress/cache/deps/_metadata.json b/docs/.vitepress/cache/deps/_metadata.json index db69e3d..484f60e 100644 --- a/docs/.vitepress/cache/deps/_metadata.json +++ b/docs/.vitepress/cache/deps/_metadata.json @@ -1,31 +1,31 @@ { - "hash": "1cdb2662", + "hash": "e6fe7b68", "configHash": "6bb8cdf2", - "lockfileHash": "f84ebc7b", - "browserHash": "32d6c109", + "lockfileHash": "4a58e439", + "browserHash": "30c24cb6", "optimized": { "vue": { "src": "../../../../node_modules/.pnpm/vue@3.5.12_typescript@5.6.3/node_modules/vue/dist/vue.runtime.esm-bundler.js", "file": "vue.js", - "fileHash": "2029d163", + "fileHash": "0114871d", "needsInterop": false }, "vitepress > @vue/devtools-api": { "src": "../../../../node_modules/.pnpm/@vue+devtools-api@7.5.4/node_modules/@vue/devtools-api/dist/index.js", "file": "vitepress___@vue_devtools-api.js", - "fileHash": "02f5f3ec", + "fileHash": "cec2fd64", "needsInterop": false }, "vitepress > @vueuse/core": { "src": "../../../../node_modules/.pnpm/@vueuse+core@11.1.0_vue@3.5.12_typescript@5.6.3_/node_modules/@vueuse/core/index.mjs", "file": "vitepress___@vueuse_core.js", - "fileHash": "b0f77eba", + "fileHash": "75c9c5c6", "needsInterop": false }, "@shikijs/vitepress-twoslash/client": { "src": "../../../../node_modules/.pnpm/@shikijs+vitepress-twoslash@1.22.1_@nuxt+kit@3.13.2_magicast@0.3.5_rollup@4.24.0_webpack-sources@3.2.3__typescript@5.6.3/node_modules/@shikijs/vitepress-twoslash/dist/client.mjs", "file": "@shikijs_vitepress-twoslash_client.js", - "fileHash": "bb186c04", + "fileHash": "43aaa425", "needsInterop": false } }, diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index 145cb6d..3e46519 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -17,6 +17,8 @@ export default defineConfig({ target: ScriptTarget.ES2022, module: ModuleKind.ES2022, moduleResolution: ModuleResolutionKind.Bundler, + experimentalDecorators: true, + emitDecoratorMetadata: true, }, }, }), @@ -28,10 +30,10 @@ export default defineConfig({ label: '简体中文', lang: 'zh', }, - en: { - label: 'English', - lang: 'en', - }, + // en: { + // label: 'English', + // lang: 'en', + // }, }, themeConfig: { @@ -40,16 +42,30 @@ export default defineConfig({ { text: '指南', items: [ - { text: 'RPC指南', link: '/rpc-guide' }, + { text: 'IOC 指南', link: '/ioc-guide' }, + { text: 'RPC 指南', link: '/rpc-guide' }, + { text: 'Restful 指南', link: '/restful-guide' }, ], }, ], sidebar: [ { - text: 'RPC指南', + text: 'IOC 指南', + items: [ + { text: '开始使用 IOC', link: '/ioc-guide' }, + ], + }, + { + text: 'RPC 指南', + items: [ + { text: '开始使用 RPC', link: '/rpc-guide' }, + ], + }, + { + text: 'Restful 指南', items: [ - { text: '开始使用', link: '/rpc-guide' }, + { text: '开始使用 Restful', link: '/restful-guide' }, ], }, ], diff --git a/docs/en/index.md b/docs/en/index.md index 2cefe1d..b6d04fd 100644 --- a/docs/en/index.md +++ b/docs/en/index.md @@ -12,7 +12,7 @@ hero: actions: - theme: brand text: Quick Start - link: /rpc-guide + link: /ioc-guide # - theme: alt # text: API Examples # link: /api-examples diff --git a/docs/index.md b/docs/index.md index 9b62eaa..0501237 100644 --- a/docs/index.md +++ b/docs/index.md @@ -12,7 +12,7 @@ hero: actions: - theme: brand text: 快速开始 - link: /rpc-guide + link: /ioc-guide # - theme: alt # text: API Examples # link: /api-examples diff --git a/docs/ioc-guide/index.md b/docs/ioc-guide/index.md new file mode 100644 index 0000000..4c592a3 --- /dev/null +++ b/docs/ioc-guide/index.md @@ -0,0 +1,39 @@ +# 指南 + +Naily.js的核心是一个以`IOC`(即控制反转)为核心的框架,它的目标是提供一个`轻量级` `易扩展`的毛坯房,让你可以根据自己的需求来进行装修。 + +## 捆绑包大小 + +你可以使用[pkg-size.dev](https://pkg-size.dev/)来查看捆绑包的大小。 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
捆绑包安装大小捆绑包大小
核心容器
后端支持
RPC支持
unplugin-rpc插件
diff --git a/docs/restful-guide/index.md b/docs/restful-guide/index.md new file mode 100644 index 0000000..36c9321 --- /dev/null +++ b/docs/restful-guide/index.md @@ -0,0 +1,50 @@ +# Restful 指南 + +传统的Restful API同样受Naily.js支持。和`Spring`/`Cell.js`/`Nest.js`/`Midway.js`等一众`IOC框架`一样,Naily.js也提供了一套`Restful`的编程模型。 + +::: tip 注意 +虽然Naily.js支持Restful API,但是我们更主推`Naily RPC`。Restful的理念长久以来一直受到争议,如果是新项目,我们更推荐使用`Naily RPC`,方便快捷创建全栈应用。 +::: + +## 创建 Restful API + +类似其他的`IOC`框架,我们可以通过 `@RestController` `@Get` 等来创建一个Restful API。 + +```typescript twoslash +// welcome.controller.ts +import { Get, RestController } from '@nailyjs/backend' + +@RestController() +export class UserController { + @Get() + getUsers() { + return 'Hello, World!' + } +} +``` + +## 启动应用程序 + +`naily.js`内部参考`nest.js`的架构提供了一个`Adapter`,但是这个`Adapter`架构默认不和`nest.js`一样用来切换`express`/`fastify`等底层框架,而是用来切换`node.js`/`bun`/`deno`等运行时环境的。 + +比如下面一个例子,我们从`@nailyjs/backend`的分包`node-adapter`中导入`NodeBootstrap`,就可以在`node.js`环境下创建一个`HTTP`服务器并启动它了。 + +```typescript twoslash +// main.ts +import { NodeBootstrap } from '@nailyjs/backend/node-adapter' +import './welcome.controller' + +new NodeBootstrap() + .run(3000) + .then(() => console.log(`Backend started on port http://localhost:3000`)) +``` + +创建好了服务器,我们需要导入刚刚我们在`welcome.controller.ts`中创建的`UserController`,这样我们的`UserController`才能被`NodeBootstrap`扫描到。 + +::: tip +目前`naily.js`只支持`node.js`环境,后续会支持更多的运行时环境,如`bun`等。 + +以后我们会提供更多的`Adapter`,比如`node-express-adapter`/`node-fastify-adapter`等,但是大概率不会封装在`@nailyjs/backend`中,而是单独的分包。 +::: + +同样这里也是参考了`cell.js`的机制,但是启动器这块比`cell.js`的`export default autoBind()`更加的透明,不会让初学者抓不着头脑。 diff --git a/fixtures/backend/naily.config.ts b/fixtures/backend/naily.config.ts new file mode 100644 index 0000000..6c76d97 --- /dev/null +++ b/fixtures/backend/naily.config.ts @@ -0,0 +1,3 @@ +export default { + hello: 'world', +} diff --git a/fixtures/backend/package.json b/fixtures/backend/package.json new file mode 100644 index 0000000..2bf73b9 --- /dev/null +++ b/fixtures/backend/package.json @@ -0,0 +1,22 @@ +{ + "name": "backend", + "type": "module", + "version": "0.0.4", + "private": true, + "description": "The backend fixture", + "author": "Naily Zero (https://naily.cc)", + "scripts": { + "build": "tsup", + "watch": "tsup -w", + "dev": "tsup && node dist/main.js" + }, + "dependencies": { + "@nailyjs/backend": "workspace:*", + "@nailyjs/config": "workspace:*", + "@nailyjs/ioc": "workspace:*", + "@nailyjs/jexl": "workspace:*" + }, + "devDependencies": { + "tsup": "^8.3.0" + } +} diff --git a/fixtures/backend/src/main.ts b/fixtures/backend/src/main.ts new file mode 100644 index 0000000..dc92641 --- /dev/null +++ b/fixtures/backend/src/main.ts @@ -0,0 +1,9 @@ +import { NodeBootstrap } from '@nailyjs/backend/node-adapter' +import { Configuration } from '@nailyjs/config' +import './test.controller' +import './test.filter' + +new NodeBootstrap() + .use(Configuration()) + .then(bootstrap => bootstrap.run(3000)) + .then(() => console.log(`Backend started on port http://localhost:3000`)) diff --git a/fixtures/backend/src/test.controller.ts b/fixtures/backend/src/test.controller.ts new file mode 100644 index 0000000..1b71aae --- /dev/null +++ b/fixtures/backend/src/test.controller.ts @@ -0,0 +1,20 @@ +/* eslint-disable ts/consistent-type-imports */ + +import { Get, RestController } from '@nailyjs/backend' +import { Value } from '@nailyjs/config' +import { Autowired } from '@nailyjs/ioc' +import { JexlExecutor } from '@nailyjs/jexl' + +@RestController() +export class TestController { + @Value('1 + 1') + private readonly hello: string + + @Autowired() + private readonly jexl: JexlExecutor + + @Get() + getString(): string { + return 'Hello World' + } +} diff --git a/fixtures/backend/src/test.filter.ts b/fixtures/backend/src/test.filter.ts new file mode 100644 index 0000000..e1a6e16 --- /dev/null +++ b/fixtures/backend/src/test.filter.ts @@ -0,0 +1,9 @@ +import { Catch, Filter } from '@nailyjs/ioc' + +@Filter() +export class TestFilter { + @Catch() + catch(error: unknown): void { + console.log('catch error', error) + } +} diff --git a/fixtures/backend/tsconfig.json b/fixtures/backend/tsconfig.json new file mode 100644 index 0000000..ddc0a63 --- /dev/null +++ b/fixtures/backend/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "target": "ES2022", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "module": "ES2022", + "moduleResolution": "Bundler" + }, + "include": ["src"] +} diff --git a/fixtures/backend/tsup.config.ts b/fixtures/backend/tsup.config.ts new file mode 100644 index 0000000..c5b035c --- /dev/null +++ b/fixtures/backend/tsup.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'tsup' + +export default defineConfig({ + entry: { + main: './src/main.ts', + }, + dts: true, + sourcemap: true, + clean: true, + format: ['cjs', 'esm'], +}) diff --git a/package.json b/package.json index 784fece..f06e245 100644 --- a/package.json +++ b/package.json @@ -17,8 +17,11 @@ "build:rpc": "pnpm -F @nailyjs/rpc build", "build:rpc-docs-protocol": "pnpm -F @nailyjs/rpc-docs-protocol build", "build:unplugin-rpc": "pnpm -F unplugin-rpc build", + "build:jexl": "pnpm -F @nailyjs/jexl build", + "build:config": "pnpm -F @nailyjs/config build", "play:rpc": "pnpm -F rpc-playground dev", "play:build": "pnpm -F rpc-playground build", + "dev:backend": "pnpm -F backend dev", "lint": "eslint .", "postinstall": "npx simple-git-hooks", "test": "vitest", diff --git a/packages/backend/package.json b/packages/backend/package.json index d631e26..97bb55a 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -19,11 +19,6 @@ "import": "./dist/index.js", "require": "./dist/index.cjs" }, - "./utils": { - "types": "./dist/utils.d.ts", - "import": "./dist/utils.js", - "require": "./dist/utils.cjs" - }, "./node-adapter": { "types": "./dist/node-adapter.d.ts", "import": "./dist/node-adapter.js", @@ -44,7 +39,8 @@ "prepublishOnly": "tsup" }, "dependencies": { - "@nailyjs/ioc": "workspace:*" + "@nailyjs/ioc": "workspace:*", + "path-to-regexp": "^8.2.0" }, "devDependencies": { "tsup": "^8.3.0" diff --git a/packages/backend/src/backend-adapter.ts b/packages/backend/src/backend-adapter.ts index 85c6412..b9e5797 100644 --- a/packages/backend/src/backend-adapter.ts +++ b/packages/backend/src/backend-adapter.ts @@ -15,7 +15,7 @@ export abstract class AbstractHttpAdapter extends BackendC * @return {Promise} * @memberof AbstractHttpAdapter */ - abstract listen(port: number, callback?: () => any): Promise + abstract listen(port: number, callback?: () => any): Promise /** * ### Close the server. * diff --git a/packages/backend/src/backend-container.ts b/packages/backend/src/backend-container.ts index d63023a..e1d1772 100644 --- a/packages/backend/src/backend-container.ts +++ b/packages/backend/src/backend-container.ts @@ -1,7 +1,8 @@ import type { InjectableWrapper } from '@nailyjs/ioc' -import { Container } from '@nailyjs/ioc' +import { Container, Injectable } from '@nailyjs/ioc' import { RestControllerSymbol } from './constant' +@Injectable() export class BackendContainer extends Container { private isCatchError(errors: any[] | boolean, comparisonError: unknown): boolean { if (typeof errors === 'boolean') return errors @@ -111,45 +112,9 @@ export class BackendContainer extends Container { return this } - /** - * ### Iterate over the rest controllers. - * - * This method will iterate over the rest controllers and call the callback function. - * The callback function should accept three arguments: `target`, `methodKey`, and `wrapper`. - * - `target` is the target `instance`(like {@linkcode InjectableWrapper.singletonInstance}), - * it is the instance of the class constructor or the instance of the class. - * - `methodKey` is the current method key, it is the key of the method. It is the method key of the class. - * - `wrapper` is the current class wrapper, it is the instance of the {@linkcode InjectableWrapper}. - * - * @example - * ```typescript - * RestController() - * class MyController { - * Get('/') - * index() {} - * } - * ``` - * - * It will execute when the class is a rest controller. - * - * @param {((target: Record, methodKey: string | symbol, wrapper: InjectableWrapper) => any)} callback The callback function. - * @return {Promise} The instance of the backend container. - * @memberof BackendContainer - */ - async eachRestController(callback: (target: Record, methodKey: string | symbol, wrapper: InjectableWrapper) => any): Promise { - const container = this.getInjectableContainer() - - for (const wrapper of container) { - if (!Reflect.hasMetadata(RestControllerSymbol, wrapper.getTarget())) continue - - const methodKeys = wrapper.getPrototypeKeys().filter(key => key !== 'constructor') - const instance = wrapper.getOrCreateInstance() - for (let i = 0; i < methodKeys.length; i++) { - if (typeof instance[methodKeys[i]] !== 'function') continue - await callback(instance, methodKeys[i], wrapper) - } - } - - return this + wrapperIsController(wrapper: InjectableWrapper): boolean { + return !wrapper.isFilter() + && wrapper.isInjectable() + && Reflect.hasMetadata(RestControllerSymbol, wrapper.getTarget()) } } diff --git a/packages/backend/src/class-method-executor.ts b/packages/backend/src/class-method-executor.ts new file mode 100644 index 0000000..112f7e7 --- /dev/null +++ b/packages/backend/src/class-method-executor.ts @@ -0,0 +1,80 @@ +import type { MatchResult } from 'path-to-regexp' +import type { RestfulMetadata } from './types' +import { Injectable, type InjectableWrapper } from '@nailyjs/ioc' +import { match } from 'path-to-regexp' +import { BackendContainer } from './backend-container' +import { RestControllerSymbol, RestfulMetadataSymbol } from './constant' + +export type PatternFnReturn = [InjectableWrapper, string | symbol, MatchResult>>] | undefined + +@Injectable() +export class ControllerMethodExecutor extends BackendContainer { + constructor() { + super() + } + + private patternRestfulMetadata(wrapper: InjectableWrapper, classMethodKey: string | symbol, pathname: string, method: string): PatternFnReturn { + const methodMetadata: RestfulMetadata[] = Reflect.getMetadata(RestfulMetadataSymbol, wrapper.getTarget(), classMethodKey) || [] + if (!methodMetadata.length) return + + for (let j = 0; j < methodMetadata.length; j++) { + const prefix = Reflect.getMetadata(RestControllerSymbol, wrapper.getTarget()) || '' + const mergedPathname = this.mergePathnames(prefix, methodMetadata[j].path) + const matchResult = match(mergedPathname)(pathname) + if (!matchResult) continue + if (methodMetadata[j].method === method) return [wrapper, classMethodKey, matchResult] + } + } + + private patternMethodKey(wrapper: InjectableWrapper, pathname: string, method: string): PatternFnReturn { + const methodKeys: (string | symbol)[] = wrapper.getPrototypeKeys().filter(key => key !== 'constructor') + + for (let i = 0; i < methodKeys.length; i++) { + const result = this.patternRestfulMetadata(wrapper, methodKeys[i], pathname, method) + if (result) return result + } + } + + private mergePathnames(path1: string, path2: string): string { + // 如果 path1 或 path2 是 null 或 undefined,将它们视为空字符串 + path1 = path1 || '' + path2 = path2 || '' + + // 合并两个路径,并用正则去掉多余的斜杠 + let combinedPath = `${path1}/${path2}`.replace(/\/{2,}/g, '/') + + // 如果合并后的路径不是根路径 '/', 则去除末尾的斜杠 + if (combinedPath !== '/' && combinedPath.endsWith('/')) { + combinedPath = combinedPath.slice(0, -1) + } + + // 确保路径以 '/' 开头 + return combinedPath.startsWith('/') ? combinedPath : `/${combinedPath}` + } + + private patternFirstPathnameAndHttpMethod(pathname: string, method: string): PatternFnReturn { + const container = this.getInjectableContainer() + + for (const wrapper of container) { + if (!this.wrapperIsController(wrapper)) continue + const result = this.patternMethodKey(wrapper, pathname, method) + if (result) return result + } + } + + private getHandler(pathname: string, method: string): ((...args: any[]) => any) | undefined { + const result = this.patternFirstPathnameAndHttpMethod(pathname, method) + if (!result) return + const [wrapper, classMethodKey] = result + + const instance = wrapper.getOrCreateInstance() + return (instance[classMethodKey] as (...args: any[]) => any).bind(instance) + } + + public async executeResult(request: Request): Promise { + const { pathname } = new URL(request.url || '', 'http://localhost') + const handler = this.getHandler(pathname, request.method) + if (!handler) return new Response('Not Found', { status: 404 }) + return await handler(request) + } +} diff --git a/packages/backend/src/constant.ts b/packages/backend/src/constant.ts index 65bebf9..31082b9 100644 --- a/packages/backend/src/constant.ts +++ b/packages/backend/src/constant.ts @@ -1,2 +1,3 @@ export const RestControllerSymbol = Symbol('__naily_rest_controller__') +export const RestfulMetadataSymbol = Symbol('__naily_restful_metadata__') export const SkipHandle = Symbol('__naily_skip_handle__') diff --git a/packages/backend/src/decorators/index.ts b/packages/backend/src/decorators/index.ts index 532fb95..df03223 100644 --- a/packages/backend/src/decorators/index.ts +++ b/packages/backend/src/decorators/index.ts @@ -1 +1,2 @@ export * from './rest-controller.decorator' +export * from './restful.decorator' diff --git a/packages/backend/src/decorators/restful.decorator.ts b/packages/backend/src/decorators/restful.decorator.ts new file mode 100644 index 0000000..04e57dd --- /dev/null +++ b/packages/backend/src/decorators/restful.decorator.ts @@ -0,0 +1,74 @@ +import type { RestfulMetadata } from '../types' +import { RestfulMetadataSymbol } from '../constant' + +export function Get(path: string = '/'): MethodDecorator { + return ((target, propertyKey) => { + Reflect.defineMetadata(RestfulMetadataSymbol, [ + ...Reflect.getMetadata(RestfulMetadataSymbol, target.constructor, propertyKey) || [], + { method: 'GET', path }, + ] as RestfulMetadata[], target.constructor, propertyKey) + }) as MethodDecorator +} + +export function Post(path: string = '/'): MethodDecorator { + return ((target, propertyKey) => { + Reflect.defineMetadata(RestfulMetadataSymbol, [ + ...Reflect.getMetadata(RestfulMetadataSymbol, target.constructor, propertyKey) || [], + { method: 'POST', path }, + ] as RestfulMetadata[], target.constructor, propertyKey) + }) as MethodDecorator +} + +export function Put(path: string = '/'): MethodDecorator { + return ((target, propertyKey) => { + Reflect.defineMetadata(RestfulMetadataSymbol, [ + ...Reflect.getMetadata(RestfulMetadataSymbol, target.constructor, propertyKey) || [], + { method: 'PUT', path }, + ] as RestfulMetadata[], target.constructor, propertyKey) + }) as MethodDecorator +} + +export function Delete(path: string = '/'): MethodDecorator { + return ((target, propertyKey) => { + Reflect.defineMetadata(RestfulMetadataSymbol, [ + ...Reflect.getMetadata(RestfulMetadataSymbol, target.constructor, propertyKey) || [], + { method: 'DELETE', path }, + ] as RestfulMetadata[], target.constructor, propertyKey) + }) as MethodDecorator +} + +export function Patch(path: string = '/'): MethodDecorator { + return ((target, propertyKey) => { + Reflect.defineMetadata(RestfulMetadataSymbol, [ + ...Reflect.getMetadata(RestfulMetadataSymbol, target.constructor, propertyKey) || [], + { method: 'PATCH', path }, + ] as RestfulMetadata[], target.constructor, propertyKey) + }) as MethodDecorator +} + +export function Head(path: string = '/'): MethodDecorator { + return ((target, propertyKey) => { + Reflect.defineMetadata(RestfulMetadataSymbol, [ + ...Reflect.getMetadata(RestfulMetadataSymbol, target.constructor, propertyKey) || [], + { method: 'HEAD', path }, + ] as RestfulMetadata[], target.constructor, propertyKey) + }) as MethodDecorator +} + +export function Options(path: string = '/'): MethodDecorator { + return ((target, propertyKey) => { + Reflect.defineMetadata(RestfulMetadataSymbol, [ + ...Reflect.getMetadata(RestfulMetadataSymbol, target.constructor, propertyKey) || [], + { method: 'OPTIONS', path }, + ] as RestfulMetadata[], target.constructor, propertyKey) + }) as MethodDecorator +} + +export function All(path: string = '/'): MethodDecorator { + return ((target, propertyKey) => { + Reflect.defineMetadata(RestfulMetadataSymbol, [ + ...Reflect.getMetadata(RestfulMetadataSymbol, target.constructor, propertyKey) || [], + { method: 'ALL', path }, + ] as RestfulMetadata[], target.constructor, propertyKey) + }) as MethodDecorator +} diff --git a/packages/backend/src/handler-context.ts b/packages/backend/src/handler-context.ts index e07be97..afc61f6 100644 --- a/packages/backend/src/handler-context.ts +++ b/packages/backend/src/handler-context.ts @@ -1,11 +1,14 @@ import type { SkipHandle } from './constant' import { BackendContainer } from './backend-container' +import { ControllerMethodExecutor } from './class-method-executor' export interface HandlerRequest extends Request {} export interface HandlerResponse extends Response {} -export abstract class HandlerContext extends BackendContainer { +export class HandlerContext extends BackendContainer { + private readonly methodExecutor = new ControllerMethodExecutor() + /** * ### The request handler callback. * @@ -25,7 +28,18 @@ export abstract class HandlerContext extends BackendContainer { * @see Response object: [MDN Reference](https://developer.mozilla.org/zh-CN/docs/Web/API/Response) * @memberof HandlerContext */ - abstract callback(request: HandlerRequest): HandlerResponse | typeof SkipHandle | Promise + async callback(request: HandlerRequest): Promise { + try { + const result = await this.methodExecutor.executeResult(request) + if (!result) return new Response('', { status: 404, statusText: 'Not Found' }) + else if (result instanceof Response) return result + else if (typeof result === 'object') return new Response(JSON.stringify(result), { status: 200, statusText: 'OK' }) + else return new Response(result, { status: 200, statusText: 'OK' }) + } + catch (error) { await this.catchError(error) } + finally { await this.catchFinally() } + } + /** * ### Handle the {@linkcode callback} `error`. * @@ -38,7 +52,13 @@ export abstract class HandlerContext extends BackendContainer { * @return {(void | Promise)} * @memberof HandlerContext */ - abstract catchError(error: unknown, ...args: any[]): any | Promise + async catchError(error: unknown, ...args: any[]): Promise { + return await this.eachErrorHandler( + async (target, methodKey) => await target[methodKey](error, ...args), + error, + ) + } + /** * ### Handle the {@linkcode callback} `finally`. * @@ -50,5 +70,10 @@ export abstract class HandlerContext extends BackendContainer { * @return {(void | Promise)} * @memberof HandlerContext */ - abstract catchFinally(...args: any[]): any | Promise + async catchFinally(...args: any[]): Promise { + return await this.eachFinallyHandler( + async (target, methodKey) => await target[methodKey](...args), + args[0], + ) + } } diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 474ced0..09962f6 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -1,6 +1,7 @@ export * from './backend-adapter' export * from './backend-bootstrap' export * from './backend-container' +export * from './class-method-executor' export * from './constant' export * from './decorators' export * from './handler-context' diff --git a/packages/backend/src/node-adapter.ts b/packages/backend/src/node-adapter.ts index 1111c4d..520f0e6 100644 --- a/packages/backend/src/node-adapter.ts +++ b/packages/backend/src/node-adapter.ts @@ -1,41 +1 @@ -import type { Server } from 'node:http' -import type { HandlerContext } from './handler-context' -import http from 'node:http' -import { AbstractHttpAdapter } from './backend-adapter' -import { SkipHandle } from './constant' -import { sendResponse, transformIncomingMessageToRequest } from './utils' - -export class NodeHttpAdapter extends AbstractHttpAdapter { - private readonly server = http.createServer() - - listen(port: number, callback?: () => any): Promise { - return new Promise((resolve) => { - this.server.listen(port, () => { - if (callback) callback() - resolve() - }) - }) - } - - close(): Promise { - return new Promise((resolve, reject) => - this.server.close((err) => { - if (err) reject(err) - else resolve() - }), - ) - } - - setupHandler(ctx: HandlerContext): void | Promise { - this.server.on('request', async (req, res) => { - const request = await transformIncomingMessageToRequest(req).getRequest() - const response = await ctx.callback(request) - if (response === SkipHandle) return - return await sendResponse(response, res).send() - }) - } - - getInstance(): Server { - return this.server - } -} +export * from './node' diff --git a/packages/backend/src/node/http-adapter.ts b/packages/backend/src/node/http-adapter.ts new file mode 100644 index 0000000..1992261 --- /dev/null +++ b/packages/backend/src/node/http-adapter.ts @@ -0,0 +1,46 @@ +import type { IncomingMessage, Server, ServerResponse } from 'node:http' +import type { HandlerContext } from '../handler-context' +import http from 'node:http' +import { AbstractHttpAdapter } from '../backend-adapter' +import { SkipHandle } from '../constant' +import { sendResponse, transformIncomingMessageToRequest } from './utils' + +export class NodeHttpAdapter extends AbstractHttpAdapter { + private serverCallback: (req: http.IncomingMessage, res: http.ServerResponse) => any + private readonly server: Server = http.createServer(async (req, res) => { + if (!this.serverCallback) throw new Error('Server callback is not set, please call setupHandler() before listen the server.') + return await this.serverCallback(req, res) + }) + + listen(port: number, callback?: (server: Server>) => any): Promise>> { + return new Promise>>((resolve) => { + this.server.listen(port, () => { + if (callback) callback(this.server) + resolve(this.server) + }) + }) + } + + close(): Promise { + return new Promise((resolve, reject) => + this.server.close((err) => { + if (err) reject(err) + else resolve() + }), + ) + } + + setupHandler(ctx: HandlerContext): void | Promise { + this.serverCallback = async (req, res) => { + const request = await transformIncomingMessageToRequest(req).getRequest() + const response = await ctx.callback(request) + if (response === SkipHandle) return + if (!response) return res.end() + return await sendResponse(response, res).send() + } + } + + getInstance(): Server { + return this.server + } +} diff --git a/packages/backend/src/node/index.ts b/packages/backend/src/node/index.ts new file mode 100644 index 0000000..3881b21 --- /dev/null +++ b/packages/backend/src/node/index.ts @@ -0,0 +1,3 @@ +export * from './http-adapter' +export * from './node-bootstrap' +export * from './utils' diff --git a/packages/backend/src/node/node-bootstrap.ts b/packages/backend/src/node/node-bootstrap.ts new file mode 100644 index 0000000..7c2f3ec --- /dev/null +++ b/packages/backend/src/node/node-bootstrap.ts @@ -0,0 +1,41 @@ +import type { InjectableWrapper } from '@nailyjs/ioc' +import type { IncomingMessage, Server, ServerResponse } from 'node:http' +import { BackendBootstrap } from '../backend-bootstrap' +import { RestControllerSymbol } from '../constant' +import { HandlerContext } from '../handler-context' +import { NodeHttpAdapter } from './http-adapter' + +export class NodeBootstrap extends BackendBootstrap { + constructor() { + super(new NodeHttpAdapter()) + } + + private wrapperIsController(wrapper: InjectableWrapper): boolean { + return !wrapper.isFilter() + && wrapper.isInjectable() + && Reflect.hasMetadata(RestControllerSymbol, wrapper.getTarget()) + } + + private isCalledInit: boolean = false + init(): this { + if (this.isCalledInit) return this + this.getInjectableContainer().forEach((wrapper) => { + if (!this.wrapperIsController(wrapper)) return + return wrapper.getOrCreateInstance() + }) + return this + } + + async run(port: number, callback?: (server: Server>) => any): Promise>> { + if (!this.isCalledInit) this.init() + const adapter = this.getBackendAdapter() + await adapter.setupHandler(new HandlerContext()) + + return new Promise>>((resolve) => { + adapter.listen(port, async (server) => { + if (callback) await callback(server) + resolve(server) + }) + }) + } +} diff --git a/packages/backend/src/utils.ts b/packages/backend/src/node/utils.ts similarity index 94% rename from packages/backend/src/utils.ts rename to packages/backend/src/node/utils.ts index d245c70..17778b0 100644 --- a/packages/backend/src/utils.ts +++ b/packages/backend/src/node/utils.ts @@ -132,13 +132,15 @@ export interface SendResponseReturn { export function sendResponse(response: Response, serverResponse: ServerResponse): SendResponseReturn { function readHeaders(response: Response): IncomingHttpHeaders { const headers: IncomingHttpHeaders = {} - for (const [key, value] of response.headers) { + for (const [key, value] of response.headers || new Headers()) { headers[key] = value } return headers } function readBody(body: ReadableStream): Promise { + if (!body) return Promise.resolve('') + const reader = body.getReader() let result = '' return reader.read().then(function processText({ done, value }) { @@ -151,9 +153,8 @@ export function sendResponse(response: Response, serverResponse: ServerResponse< } async function send(): Promise> { - return serverResponse - .writeHead(response.status, response.statusText, readHeaders(response)) - .end(await readBody(response.body)) + serverResponse.writeHead(response.status, response.statusText, readHeaders(response)) + return serverResponse.end(await readBody(response.body)) } return { diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts new file mode 100644 index 0000000..ca7b778 --- /dev/null +++ b/packages/backend/src/types.ts @@ -0,0 +1,4 @@ +export interface RestfulMetadata { + method: string + path: string +} diff --git a/packages/backend/tsup.config.ts b/packages/backend/tsup.config.ts index 328222b..897d39a 100644 --- a/packages/backend/tsup.config.ts +++ b/packages/backend/tsup.config.ts @@ -3,7 +3,6 @@ import { defineConfig } from 'tsup' export default defineConfig({ entry: { 'index': './src/index.ts', - 'utils': './src/utils.ts', 'node-adapter': './src/node-adapter.ts', }, dts: true, diff --git a/packages/cache/package.json b/packages/cache/package.json new file mode 100644 index 0000000..0288049 --- /dev/null +++ b/packages/cache/package.json @@ -0,0 +1,27 @@ +{ + "name": "@nailyjs/cache", + "type": "module", + "version": "0.0.4", + "description": "Cache manager for Naily.js", + "author": "Naily Zero (https://naily.cc)", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "require": "./dist/index.cjs" + } + }, + "main": "./dist/index.cjs", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "publishConfig": { + "access": "public" + }, + "scripts": { + "build": "tsup", + "watch": "tsup -w" + }, + "devDependencies": { + "tsup": "^8.3.0" + } +} diff --git a/packages/cache/src/index.ts b/packages/cache/src/index.ts new file mode 100644 index 0000000..f60ea9d --- /dev/null +++ b/packages/cache/src/index.ts @@ -0,0 +1 @@ +export default 'Hello, world!' diff --git a/packages/cache/tsconfig.json b/packages/cache/tsconfig.json new file mode 100644 index 0000000..551ed97 --- /dev/null +++ b/packages/cache/tsconfig.json @@ -0,0 +1,8 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "Bundler" + }, + "include": ["src"] +} diff --git a/packages/cache/tsup.config.ts b/packages/cache/tsup.config.ts new file mode 100644 index 0000000..31fd75b --- /dev/null +++ b/packages/cache/tsup.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'tsup' + +export default defineConfig({ + entry: { + index: './src/index.ts', + }, + dts: true, + sourcemap: true, + clean: true, + format: ['cjs', 'esm'], +}) diff --git a/packages/config/package.json b/packages/config/package.json new file mode 100644 index 0000000..8c066a7 --- /dev/null +++ b/packages/config/package.json @@ -0,0 +1,32 @@ +{ + "name": "@nailyjs/config", + "type": "module", + "version": "0.0.4", + "description": "Config module for Naily.js", + "author": "Naily Zero (https://naily.cc)", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "require": "./dist/index.cjs" + } + }, + "main": "./dist/index.cjs", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "publishConfig": { + "access": "public" + }, + "scripts": { + "build": "tsup", + "watch": "tsup -w" + }, + "dependencies": { + "@nailyjs/ioc": "workspace:*", + "@nailyjs/jexl": "workspace:*", + "c12": "^2.0.1" + }, + "devDependencies": { + "tsup": "^8.3.0" + } +} diff --git a/packages/config/src/decorators/index.ts b/packages/config/src/decorators/index.ts new file mode 100644 index 0000000..ae503d3 --- /dev/null +++ b/packages/config/src/decorators/index.ts @@ -0,0 +1 @@ +export * from './value.decorator' diff --git a/packages/config/src/decorators/value.decorator.ts b/packages/config/src/decorators/value.decorator.ts new file mode 100644 index 0000000..59b2ee8 --- /dev/null +++ b/packages/config/src/decorators/value.decorator.ts @@ -0,0 +1,18 @@ +export interface ValueMetadata { + path: string + propertyKey: string | symbol + parameterIndex: number +} + +export function Value(path: string = ''): ParameterDecorator & PropertyDecorator { + return ((target: Object, propertyKey: string | symbol, parameterIndex) => { + Reflect.defineMetadata('__value__', [ + ...(Reflect.getMetadata('__value__', target.constructor) || []), + { + path, + propertyKey, + parameterIndex, + }, + ] as ValueMetadata[], target.constructor) + }) as ParameterDecorator & PropertyDecorator +} diff --git a/packages/config/src/index.ts b/packages/config/src/index.ts new file mode 100644 index 0000000..6eedd87 --- /dev/null +++ b/packages/config/src/index.ts @@ -0,0 +1,3 @@ +export * from './decorators' +export * from './plugin' +export * from './types' diff --git a/packages/config/src/plugin.ts b/packages/config/src/plugin.ts new file mode 100644 index 0000000..fa0cf44 --- /dev/null +++ b/packages/config/src/plugin.ts @@ -0,0 +1,57 @@ +import type { Container, PluginProtocol } from '@nailyjs/ioc' +import type { ValueMetadata } from './decorators' +import { InjectableWrapper } from '@nailyjs/ioc' +import { JexlExecutor } from '@nailyjs/jexl' +import { loadConfig } from 'c12' + +class ConfigurationPlugin implements PluginProtocol { + private readConfiguration(): ReturnType { + return loadConfig({ + name: 'naily', + }) + } + + async install(bootstrap: Container): Promise { + const injectableContainer = bootstrap.getInjectableContainer() + const jexlExecutor: JexlExecutor = InjectableWrapper.getOrCreateInjectableWrapper(JexlExecutor).getOrCreateInstance() + const configuration = await this.readConfiguration() + const newInjectableContainer = new Set() + + for (const wrapper of injectableContainer) { + const target = wrapper.getTarget() + const valueMetadata: ValueMetadata[] = Reflect.getMetadata('__value__', target) || [] + + const createRawInstance = wrapper.createRawInstance.bind(wrapper) + wrapper.createRawInstance = function (args) { + for (const metadata of valueMetadata) { + const { path, parameterIndex } = metadata + if (typeof parameterIndex !== 'number') continue + if (!path) { + args[parameterIndex] = configuration.config + continue + } + const value = jexlExecutor.evalSync(path, configuration.config) + args[parameterIndex] = value + } + const instance = createRawInstance(args) + for (const metadata of valueMetadata) { + const { path, propertyKey, parameterIndex } = metadata + if (typeof parameterIndex === 'number') continue + if (!path) { + instance[propertyKey] = configuration.config + continue + } + const value = jexlExecutor.evalSync(path, configuration.config) + instance[propertyKey] = value + } + return instance + } + newInjectableContainer.add(wrapper) + } + bootstrap.replaceInjectableContainer(newInjectableContainer) + } +} + +export function Configuration(): PluginProtocol { + return new ConfigurationPlugin() +} diff --git a/packages/config/src/types.ts b/packages/config/src/types.ts new file mode 100644 index 0000000..8156b27 --- /dev/null +++ b/packages/config/src/types.ts @@ -0,0 +1,43 @@ +/** + * Evaluates to `true` if `T` is `any`. `false` otherwise. + * (c) https://stackoverflow.com/a/68633327/5290447 + */ +type IsAny = unknown extends T + ? [keyof T] extends [never] + ? false + : true + : false + +export type PathImpl = Key extends string + ? IsAny extends true + ? never + : T[Key] extends Record + ? + | `${Key}.${PathImpl> & + string}` + | `${Key}.${Exclude & string}` + : never + : never + +export type PathImpl2 = PathImpl | keyof T + +export type Path = keyof T extends string + ? PathImpl2 extends infer P + ? P extends string | keyof T + ? P + : keyof T + : keyof T + : never + +export type PathValue< + T, + P extends Path, +> = P extends `${infer Key}.${infer Rest}` + ? Key extends keyof T + ? Rest extends Path + ? PathValue + : never + : never + : P extends keyof T + ? T[P] + : never diff --git a/packages/config/tsconfig.json b/packages/config/tsconfig.json new file mode 100644 index 0000000..ddc0a63 --- /dev/null +++ b/packages/config/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "target": "ES2022", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "module": "ES2022", + "moduleResolution": "Bundler" + }, + "include": ["src"] +} diff --git a/packages/config/tsup.config.ts b/packages/config/tsup.config.ts new file mode 100644 index 0000000..31fd75b --- /dev/null +++ b/packages/config/tsup.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'tsup' + +export default defineConfig({ + entry: { + index: './src/index.ts', + }, + dts: true, + sourcemap: true, + clean: true, + format: ['cjs', 'esm'], +}) diff --git a/packages/ioc/src/abstract-bootstrap.ts b/packages/ioc/src/abstract-bootstrap.ts index 2936bd8..5ffe4a9 100644 --- a/packages/ioc/src/abstract-bootstrap.ts +++ b/packages/ioc/src/abstract-bootstrap.ts @@ -1,4 +1,5 @@ import type { ContainerProtocol } from './container-protocol' +import type { PluginProtocol } from './plugin-protocol' import { Container } from './container' export abstract class AbstractBootstrap extends Container implements ContainerProtocol { @@ -51,4 +52,9 @@ export abstract class AbstractBootstrap extends Container implements ContainerPr static isClient(): boolean { return typeof window !== 'undefined' && typeof document !== 'undefined' } + + async use(plugin: PluginProtocol): Promise { + await plugin.install(this) + return this + } } diff --git a/packages/ioc/src/constants/container-constant.ts b/packages/ioc/src/constants/container-constant.ts deleted file mode 100644 index b7463b8..0000000 --- a/packages/ioc/src/constants/container-constant.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { InjectOptions } from '../decorators' -import type { InjectableWrapper } from '../injectable-wrapper' - -export const MarkedInjectable = new Set() -export const MarkedInject = new Set>() diff --git a/packages/ioc/src/container-protocol.ts b/packages/ioc/src/container-protocol.ts index ac579ed..f7baefe 100644 --- a/packages/ioc/src/container-protocol.ts +++ b/packages/ioc/src/container-protocol.ts @@ -1,4 +1,4 @@ -import type { InjectOptions } from './decorators' +import type { InjectWrapper } from './inject-wrapper' import type { InjectableWrapper } from './injectable-wrapper' import type { Class } from './types' @@ -16,7 +16,7 @@ export interface ContainerProtocol { * @return {Set>} * @memberof ContainerProtocol */ - getInjectContainer(): Set> + getInjectContainer(): Set /** * ### Get the Injectable Target. * diff --git a/packages/ioc/src/container.ts b/packages/ioc/src/container.ts index eb6b58e..76371ff 100644 --- a/packages/ioc/src/container.ts +++ b/packages/ioc/src/container.ts @@ -1,23 +1,47 @@ import type { ContainerProtocol } from './container-protocol' -import type { InjectOptions } from './decorators' +import type { InjectWrapper } from './inject-wrapper' import type { InjectableWrapper } from './injectable-wrapper' -import type { Class } from './types' -import { MarkedInject, MarkedInjectable } from './constants/container-constant' +import type { Class, InjectionToken } from './types' export class Container implements ContainerProtocol { - getInjectContainer(): Set> { - return MarkedInject + private static markedInjectable = new Set() + private static markedInject = new Set() + + getInjectContainer(): Set { + return Container.markedInject } getInjectableContainer(): Set { - return MarkedInjectable + return Container.markedInjectable } getInjectableTarget(comparisonTarget: Class): InjectableWrapper | undefined { - return Array.from(MarkedInjectable).find(wrapper => wrapper.getTarget() === comparisonTarget) as InjectableWrapper + if (!comparisonTarget) return + return Array.from(Container.markedInjectable).find(wrapper => wrapper.getTarget() === comparisonTarget) as InjectableWrapper + } + + getInjectableTargetByToken(comparisonToken: InjectionToken): InjectableWrapper | undefined { + if (!comparisonToken) return + return Array.from(Container.markedInjectable).find(wrapper => wrapper.getInjectableOptions().injectionToken === comparisonToken) as InjectableWrapper + } + + getInjectableByTargetOrToken(comparison: Class | InjectionToken): InjectableWrapper | undefined { + if (!comparison) return + return Array.from(Container.markedInjectable).find((wrapper) => { + const injectableOptions = wrapper.getInjectableOptions() + return injectableOptions.injectionToken === comparison || wrapper.getTarget() === comparison + }) as InjectableWrapper } hasInjectableTarget(comparisonTarget: Class): boolean { - return Array.from(MarkedInjectable).some(wrapper => wrapper.getTarget() === comparisonTarget) + return Array.from(Container.markedInjectable).some(wrapper => wrapper.getTarget() === comparisonTarget) + } + + replaceInjectableContainer(injectableContainer: Set): void { + Container.markedInjectable = injectableContainer + } + + replaceInjectContainer(injectContainer: Set): void { + Container.markedInject = injectContainer } } diff --git a/packages/ioc/src/decorators/inject.decorator.ts b/packages/ioc/src/decorators/inject.decorator.ts index 021b95e..16748d8 100644 --- a/packages/ioc/src/decorators/inject.decorator.ts +++ b/packages/ioc/src/decorators/inject.decorator.ts @@ -1,6 +1,7 @@ import type { InjectionToken } from '../types' import { InjectSymbol } from '../constants/constant' -import { MarkedInject } from '../constants/container-constant' +import { Container } from '../container' +import { InjectWrapper } from '../inject-wrapper' export interface InjectOptions { injectionToken: InjectionToken @@ -16,7 +17,8 @@ export function Inject(options?: Partial): PropertyDecorator & Pa currentTarget: target, } Reflect.defineMetadata(InjectSymbol, metadata, target.constructor, propertyKey) - MarkedInject.add(metadata) + // eslint-disable-next-line dot-notation + Container['markedInject'].add(new InjectWrapper(metadata)) }) as PropertyDecorator & ParameterDecorator } diff --git a/packages/ioc/src/decorators/injectable.decorator.ts b/packages/ioc/src/decorators/injectable.decorator.ts index 1dc245b..1fd70d2 100644 --- a/packages/ioc/src/decorators/injectable.decorator.ts +++ b/packages/ioc/src/decorators/injectable.decorator.ts @@ -1,6 +1,6 @@ -import type { Class, ScopeType } from '../types' +import type { Class, InjectionToken, ScopeType } from '../types' import { InjectableSymbol } from '../constants/constant' -import { MarkedInjectable } from '../constants/container-constant' +import { Container } from '../container' import { InjectableWrapper } from '../injectable-wrapper' import 'reflect-metadata' @@ -19,6 +19,7 @@ export interface InjectableOptions { * @memberof InjectableOptions */ scope: ScopeType + injectionToken?: InjectionToken | InjectionToken[] } /** @@ -36,7 +37,8 @@ export function Injectable(options: Partial = {}): ClassDecor currentTarget: target, } Reflect.defineMetadata(InjectableSymbol, metadata, target) - MarkedInjectable.add(new InjectableWrapper(target)) + // eslint-disable-next-line dot-notation + Container['markedInjectable'].add(new InjectableWrapper(target)) }) as ClassDecorator } diff --git a/packages/ioc/src/index.ts b/packages/ioc/src/index.ts index 2b8f256..148f3b9 100644 --- a/packages/ioc/src/index.ts +++ b/packages/ioc/src/index.ts @@ -7,7 +7,7 @@ export * from './container-protocol' export * from './decorators' export * from './errors' export * from './injectable-wrapper' -export * from './injectable-wrapper' +export * from './plugin-protocol' export * from './types' export * from 'reflect-metadata' diff --git a/packages/ioc/src/inject-wrapper.ts b/packages/ioc/src/inject-wrapper.ts new file mode 100644 index 0000000..fff925b --- /dev/null +++ b/packages/ioc/src/inject-wrapper.ts @@ -0,0 +1,15 @@ +import type { InjectOptions } from './decorators' + +export class InjectWrapper { + constructor(private readonly injectOptions: Partial) {} + + getInjectOptions(): Partial { + const result = this.injectOptions + if (!result.injectionToken) { + const designType = Reflect.getMetadata('design:type', result.currentTarget.constructor, result.currentProperty) + if (designType && typeof designType === 'function') + result.injectionToken = designType + } + return result + } +} diff --git a/packages/ioc/src/injectable-wrapper.ts b/packages/ioc/src/injectable-wrapper.ts index 00a8e9b..b32347c 100644 --- a/packages/ioc/src/injectable-wrapper.ts +++ b/packages/ioc/src/injectable-wrapper.ts @@ -11,6 +11,16 @@ export class InjectableWrapper extends Container i super() } + static getOrCreateInjectableWrapper(target: Class): InjectableWrapper { + let injectableWrapper = new Container().getInjectableTarget(target) + if (!injectableWrapper) { + injectableWrapper = new InjectableWrapper(target) + // eslint-disable-next-line dot-notation + Container['markedInjectable'].add(injectableWrapper) + } + return injectableWrapper + } + /** * ### Get the injectable options of the target class. * @@ -216,6 +226,36 @@ export class InjectableWrapper extends Container i return dependencies } + /** + * ### Bind the property dependencies of the target class. + * + * It will be called in {@linkcode InjectableWrapper.createInstance} and {@linkcode InjectableWrapper.getOrCreateInstance}. + * We use `Object.defineProperty` to bind the property dependencies. The property will be `readonly`. + * If the property is not an injectable, it will not bind the property. + * + * @param instance The instance of the target class. + * @memberof InjectableWrapper + */ + bindPropertyDependencies(instance: Instance): void { + const injectedContainer = this.getInjectContainer() + + for (const injected of injectedContainer) { + const injectedOptions = injected.getInjectOptions() + const propertyKey = injectedOptions.currentProperty + const target = injectedOptions.currentTarget.constructor + if (target !== this.target) + continue + const designType = Reflect.getMetadata('design:type', target.prototype, propertyKey) + const wrapper = this.getInjectableByTargetOrToken(injectedOptions.injectionToken || designType) + if (!wrapper) + continue + Object.defineProperty(instance, propertyKey, { + writable: false, + value: wrapper.getOrCreateInstance(), + }) + } + } + /** * ### Create a raw instance of the target class. * @@ -245,7 +285,8 @@ export class InjectableWrapper extends Container i const dependencies = this.getConstructorDependencies(skipAllIfNotInjectable) const args = dependencies.map(dependency => dependency.getOrCreateInstance()) const rawInstance = this.createRawInstance(args) - if (this.isSingleton() && noStore === false) this.singletonInstance = rawInstance + this.bindPropertyDependencies(rawInstance) + if (this.isSingleton() && noStore !== true) this.singletonInstance = rawInstance return this.singletonInstance } @@ -267,11 +308,7 @@ export class InjectableWrapper extends Container i getOrCreateInstance(skipAllIfNotInjectable: boolean = false): InstanceType { if (this.singletonInstance !== null && this.isSingleton()) return this.singletonInstance - const dependencies = this.getConstructorDependencies(skipAllIfNotInjectable) - const args = dependencies.map(dependency => dependency.getOrCreateInstance()) - const rawInstance = this.createRawInstance(args) - if (this.isSingleton()) this.singletonInstance = rawInstance - return this.singletonInstance + return this.createInstance(skipAllIfNotInjectable) } /** diff --git a/packages/ioc/src/plugin-protocol.ts b/packages/ioc/src/plugin-protocol.ts new file mode 100644 index 0000000..b1c045a --- /dev/null +++ b/packages/ioc/src/plugin-protocol.ts @@ -0,0 +1,5 @@ +import type { Container } from './container' + +export interface PluginProtocol { + install(bootstrap: Container): any +} diff --git a/packages/jexl/package.json b/packages/jexl/package.json new file mode 100644 index 0000000..66fff1a --- /dev/null +++ b/packages/jexl/package.json @@ -0,0 +1,32 @@ +{ + "name": "@nailyjs/jexl", + "type": "module", + "version": "0.0.4", + "description": "Jexl expression engine for naily.js", + "author": "Naily Zero (https://naily.cc)", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "require": "./dist/index.cjs" + } + }, + "main": "./dist/index.cjs", + "module": "./dist/index.js", + "types": "./dist/index.d.ts", + "publishConfig": { + "access": "public" + }, + "scripts": { + "build": "tsup", + "watch": "tsup -w" + }, + "dependencies": { + "@nailyjs/ioc": "workspace:*", + "@types/jexl": "^2.3.4", + "jexl": "^2.3.0" + }, + "devDependencies": { + "tsup": "^8.3.0" + } +} diff --git a/packages/jexl/src/index.ts b/packages/jexl/src/index.ts new file mode 100644 index 0000000..b3c9633 --- /dev/null +++ b/packages/jexl/src/index.ts @@ -0,0 +1,5 @@ +import { Injectable } from '@nailyjs/ioc' +import { Jexl } from 'jexl' + +@Injectable() +export class JexlExecutor extends Jexl {} diff --git a/packages/jexl/tsconfig.json b/packages/jexl/tsconfig.json new file mode 100644 index 0000000..ddc0a63 --- /dev/null +++ b/packages/jexl/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "target": "ES2022", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "module": "ES2022", + "moduleResolution": "Bundler" + }, + "include": ["src"] +} diff --git a/packages/jexl/tsup.config.ts b/packages/jexl/tsup.config.ts new file mode 100644 index 0000000..31fd75b --- /dev/null +++ b/packages/jexl/tsup.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'tsup' + +export default defineConfig({ + entry: { + index: './src/index.ts', + }, + dts: true, + sourcemap: true, + clean: true, + format: ['cjs', 'esm'], +}) diff --git a/packages/rpc-protocol/package.json b/packages/rpc-protocol/package.json index 0dc945f..020b622 100644 --- a/packages/rpc-protocol/package.json +++ b/packages/rpc-protocol/package.json @@ -1,7 +1,7 @@ { "name": "@nailyjs/rpc-docs-protocol", "type": "module", - "version": "1.0.15", + "version": "1.0.16", "description": "Json-RPC 2.0 protocol docs implementation.", "author": "Naily Zero (https://naily.cc)", "keywords": [ diff --git a/packages/rpc-protocol/src/index.ts b/packages/rpc-protocol/src/index.ts index 484a7b2..110b4e1 100644 --- a/packages/rpc-protocol/src/index.ts +++ b/packages/rpc-protocol/src/index.ts @@ -34,7 +34,7 @@ export class JsonRpcDataSource { ...this.doc.tags, ...Object.keys(this.doc.docs).map((key) => { return this.doc.docs[key].tags - }).flat().filter(tag => tag !== undefined), + }).flat().filter(tag => tag !== undefined).filter((tag, index, self) => self.indexOf(tag) === index), ] } @@ -43,4 +43,10 @@ export class JsonRpcDataSource { return docObject.tags?.includes(tag) }) } + + getNoTagDoc(): [string, JsonRpcDocsObject][] { + return Object.entries(this.doc.docs).filter(([_key, docObject]) => { + return docObject.tags === undefined || docObject.tags.length === 0 + }) + } } diff --git a/packages/rpc/src/index.ts b/packages/rpc/src/index.ts index 65be4b7..0df487b 100644 --- a/packages/rpc/src/index.ts +++ b/packages/rpc/src/index.ts @@ -1,6 +1,7 @@ export * from './constant' export * from './decorators' export * from './rpc-bootstrap' +export * from './rpc-controller-container' export * from './rpc-handler' export * from './rpc-plugin-protocol' export * from './schema' diff --git a/packages/rpc/src/rpc-handler-context.ts b/packages/rpc/src/rpc-controller-container.ts similarity index 94% rename from packages/rpc/src/rpc-handler-context.ts rename to packages/rpc/src/rpc-controller-container.ts index 26e457a..ea1c7e9 100644 --- a/packages/rpc/src/rpc-handler-context.ts +++ b/packages/rpc/src/rpc-controller-container.ts @@ -2,7 +2,7 @@ import type { Class, InjectableWrapper } from '@nailyjs/ioc' import { BackendContainer } from '@nailyjs/backend' import { RpcControllerSymbol } from './constant' -export class RpcHandlerContext extends BackendContainer { +export class RpcControllerContainer extends BackendContainer { private isRpcController(target: Class): boolean { return Reflect.hasMetadata(RpcControllerSymbol, target) } diff --git a/packages/rpc/src/rpc-handler.ts b/packages/rpc/src/rpc-handler.ts index b0ef29e..5e43753 100644 --- a/packages/rpc/src/rpc-handler.ts +++ b/packages/rpc/src/rpc-handler.ts @@ -1,11 +1,12 @@ -import type { HandlerContext, HandlerRequest, HandlerResponse, SkipHandle } from '@nailyjs/backend' +import type { HandlerRequest, HandlerResponse, SkipHandle } from '@nailyjs/backend' import type { InjectableWrapper } from '@nailyjs/ioc' import type { z } from 'zod' import { randomUUID } from 'node:crypto' -import { RpcHandlerContext } from './rpc-handler-context' +import { HandlerContext } from '@nailyjs/backend' +import { RpcControllerContainer } from './rpc-controller-container' import { JsonRpcSchema } from './schema' -export class RpcHttpHandler extends RpcHandlerContext implements HandlerContext { +export class RpcHttpHandler extends HandlerContext { constructor(private baseURL: string = '/') { super() } @@ -19,10 +20,12 @@ export class RpcHttpHandler extends RpcHandlerContext implements HandlerContext return this.baseURL } + private readonly rpcControllerContainer = new RpcControllerContainer() + findRpcControllerWrapper(comparisonRpcId: string | symbol, comparisonMethodKey: string | symbol): Promise { // eslint-disable-next-line no-async-promise-executor return new Promise(async (resolve) => { - await this.eachRpcController((_target, key, wrapper, rpcId) => { + await this.rpcControllerContainer.eachRpcController((_target, key, wrapper, rpcId) => { if (key === comparisonMethodKey && comparisonRpcId === rpcId) resolve(wrapper) }) resolve(new Response(JSON.stringify({ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3f6fccd..8a25bcc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -96,11 +96,55 @@ importers: specifier: ^2.1.2 version: 2.1.3(@types/node@22.7.8)(@vitest/ui@2.1.3)(jsdom@24.1.3)(less@4.2.0)(terser@5.36.0) + fixtures/backend: + dependencies: + '@nailyjs/backend': + specifier: workspace:* + version: link:../../packages/backend + '@nailyjs/config': + specifier: workspace:* + version: link:../../packages/config + '@nailyjs/ioc': + specifier: workspace:* + version: link:../../packages/ioc + '@nailyjs/jexl': + specifier: workspace:* + version: link:../../packages/jexl + devDependencies: + tsup: + specifier: ^8.3.0 + version: 8.3.0(@swc/core@1.7.39(@swc/helpers@0.5.13))(jiti@2.3.3)(postcss@8.4.47)(tsx@4.19.1)(typescript@5.6.3)(yaml@2.6.0) + packages/backend: dependencies: '@nailyjs/ioc': specifier: workspace:* version: link:../ioc + path-to-regexp: + specifier: ^8.2.0 + version: 8.2.0 + devDependencies: + tsup: + specifier: ^8.3.0 + version: 8.3.0(@swc/core@1.7.39(@swc/helpers@0.5.13))(jiti@2.3.3)(postcss@8.4.47)(tsx@4.19.1)(typescript@5.6.3)(yaml@2.6.0) + + packages/cache: + devDependencies: + tsup: + specifier: ^8.3.0 + version: 8.3.0(@swc/core@1.7.39(@swc/helpers@0.5.13))(jiti@2.3.3)(postcss@8.4.47)(tsx@4.19.1)(typescript@5.6.3)(yaml@2.6.0) + + packages/config: + dependencies: + '@nailyjs/ioc': + specifier: workspace:* + version: link:../ioc + '@nailyjs/jexl': + specifier: workspace:* + version: link:../jexl + c12: + specifier: ^2.0.1 + version: 2.0.1(magicast@0.3.5) devDependencies: tsup: specifier: ^8.3.0 @@ -119,6 +163,19 @@ importers: specifier: ^8.3.0 version: 8.3.0(@swc/core@1.7.39(@swc/helpers@0.5.13))(jiti@2.3.3)(postcss@8.4.47)(tsx@4.19.1)(typescript@5.6.3)(yaml@2.6.0) + packages/jexl: + dependencies: + '@types/jexl': + specifier: ^2.3.4 + version: 2.3.4 + jexl: + specifier: ^2.3.0 + version: 2.3.0 + devDependencies: + tsup: + specifier: ^8.3.0 + version: 8.3.0(@swc/core@1.7.39(@swc/helpers@0.5.13))(jiti@2.3.3)(postcss@8.4.47)(tsx@4.19.1)(typescript@5.6.3)(yaml@2.6.0) + packages/rpc: dependencies: '@nailyjs/backend': @@ -1337,6 +1394,9 @@ packages: '@types/http-proxy@1.17.15': resolution: {integrity: sha512-25g5atgiVNTIv0LBDTg1H74Hvayx0ajtJPLLcYE3whFv75J0pWNtOBzaXJQgDTmrX1bx5U9YC2w/n65BN1HwRQ==} + '@types/jexl@2.3.4': + resolution: {integrity: sha512-3BU5DbkPvwqOeW8kZcB9Bvn5bspTFY3Lo0yBS6ephuI8kVSpCjzbGtRPbWnvdbr67tMvXDxiJRcL3fwbz/6+PQ==} + '@types/js-yaml@4.0.9': resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==} @@ -2004,6 +2064,14 @@ packages: magicast: optional: true + c12@2.0.1: + resolution: {integrity: sha512-Z4JgsKXHG37C6PYUtIxCfLJZvo6FyhHJoClwwb9ftUkLpPSkuYqn6Tr+vnaN8hymm0kIbcg6Ey3kv/Q71k5w/A==} + peerDependencies: + magicast: ^0.3.5 + peerDependenciesMeta: + magicast: + optional: true + cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} @@ -2068,6 +2136,10 @@ packages: resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} engines: {node: '>= 8.10.0'} + chokidar@4.0.1: + resolution: {integrity: sha512-n8enUVCED/KVRQlab1hr3MVpcVMvxtZjmEa956u+4YijlmQED223XMSYj2tLuKvr4jcCTzNNMpQDUer72MMmzA==} + engines: {node: '>= 14.16.0'} + chownr@2.0.0: resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} engines: {node: '>=10'} @@ -4137,6 +4209,10 @@ packages: path-to-regexp@0.1.10: resolution: {integrity: sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==} + path-to-regexp@8.2.0: + resolution: {integrity: sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==} + engines: {node: '>=16'} + path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} @@ -4315,6 +4391,10 @@ packages: resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} engines: {node: '>=8.10.0'} + readdirp@4.0.2: + resolution: {integrity: sha512-yDMz9g+VaZkqBYS/ozoBJwaBhTbZo3UNYQHNRw1D3UFQB8oHB4uS/tAODO+ZLjGWmUbKnIlOWO+aaIiAxrUWHA==} + engines: {node: '>= 14.16.0'} + refa@0.12.1: resolution: {integrity: sha512-J8rn6v4DBb2nnFqkqwy6/NnTYMcgLA+sLr0iIO41qpv0n+ngb7ksag2tMRl0inb1bbO/esUwzW1vbJi7K0sI0g==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} @@ -6665,6 +6745,8 @@ snapshots: dependencies: '@types/node': 22.7.8 + '@types/jexl@2.3.4': {} + '@types/js-yaml@4.0.9': {} '@types/json-schema@7.0.15': {} @@ -7543,6 +7625,23 @@ snapshots: optionalDependencies: magicast: 0.3.5 + c12@2.0.1(magicast@0.3.5): + dependencies: + chokidar: 4.0.1 + confbox: 0.1.8 + defu: 6.1.4 + dotenv: 16.4.5 + giget: 1.2.3 + jiti: 2.3.3 + mlly: 1.7.2 + ohash: 1.1.4 + pathe: 1.1.2 + perfect-debounce: 1.0.0 + pkg-types: 1.2.1 + rc9: 2.1.2 + optionalDependencies: + magicast: 0.3.5 + cac@6.7.14: {} cache-content-type@1.0.1: @@ -7614,6 +7713,10 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + chokidar@4.0.1: + dependencies: + readdirp: 4.0.2 + chownr@2.0.0: {} chrome-trace-event@1.0.4: {} @@ -10081,6 +10184,8 @@ snapshots: path-to-regexp@0.1.10: {} + path-to-regexp@8.2.0: {} + path-type@4.0.0: {} path-type@5.0.0: {} @@ -10229,6 +10334,8 @@ snapshots: dependencies: picomatch: 2.3.1 + readdirp@4.0.2: {} + refa@0.12.1: dependencies: '@eslint-community/regexpp': 4.11.1