From 7e9d488527c2880e21c0960ae3aac361d6fbe6ae Mon Sep 17 00:00:00 2001 From: longdayi <13477510+longdayilongdayi@user.noreply.gitee.com> Date: Fri, 2 Aug 2024 19:48:38 +0800 Subject: [PATCH] 08021948 --- apps/server/.env.example | 10 +++ apps/server/package.json | 9 +++ apps/server/src/app.module.ts | 6 +- apps/server/src/main.ts | 4 +- .../queue/general/general-queue.listener.ts | 32 ++++++++ .../queue/general/general-queue.service.ts | 23 ++++++ .../src/queue/general/general.service.ts | 17 +++++ apps/server/src/queue/queue.module.ts | 22 ++++++ apps/server/src/queue/worker/processor.ts | 5 ++ apps/server/src/redis/redis.module.ts | 3 +- apps/server/src/redis/redis.service.ts | 58 +++++++++++++- apps/server/src/socket/socket.gateway.spec.ts | 18 +++++ apps/server/src/socket/socket.gateway.ts | 28 +++++++ apps/server/src/utils/tusd.ts | 76 +++++++++++++++++++ packages/common/.env.example | 0 packages/common/package.json | 0 packages/common/prisma/schema.prisma | 0 packages/common/src/index.ts | 0 packages/common/src/schema.ts | 0 packages/common/tsconfig.cjs.json | 0 packages/common/tsconfig.esm.json | 0 packages/iconer/.eslintrc.cjs | 18 +++++ packages/iconer/README.md | 30 ++++++++ packages/iconer/package.json | 33 ++++++++ packages/iconer/public/vite.svg | 1 + packages/iconer/src/components/svg-icon.tsx | 40 ++++++++++ packages/iconer/src/generated/icon-names.ts | 2 + packages/iconer/src/icons/align-center.svg | 4 + packages/iconer/src/icons/align-justify.svg | 1 + packages/iconer/src/icons/align-left.svg | 1 + packages/iconer/src/icons/align-right.svg | 1 + packages/iconer/src/icons/arrow-drop-down.svg | 1 + packages/iconer/src/icons/bold.svg | 1 + packages/iconer/src/icons/check.svg | 1 + packages/iconer/src/icons/content.svg | 1 + packages/iconer/src/icons/copy.svg | 1 + packages/iconer/src/icons/edit.svg | 1 + packages/iconer/src/icons/get-text.svg | 1 + packages/iconer/src/icons/home.svg | 1 + packages/iconer/src/icons/horizontal-rule.svg | 1 + packages/iconer/src/icons/image.svg | 1 + packages/iconer/src/icons/italic.svg | 1 + packages/iconer/src/icons/link-off.svg | 1 + packages/iconer/src/icons/link.svg | 1 + packages/iconer/src/icons/logout.svg | 1 + packages/iconer/src/icons/react.svg | 6 ++ packages/iconer/src/icons/redo.svg | 1 + packages/iconer/src/icons/share.svg | 1 + packages/iconer/src/icons/strike.svg | 1 + packages/iconer/src/icons/text-indent.svg | 1 + packages/iconer/src/icons/text-outdent.svg | 1 + packages/iconer/src/icons/underline.svg | 1 + packages/iconer/src/icons/undo.svg | 1 + packages/iconer/src/icons/zoomin.svg | 1 + packages/iconer/src/icons/zoomout.svg | 1 + packages/iconer/src/index.css | 0 packages/iconer/src/index.ts | 7 ++ packages/iconer/src/utils/useLazySvgImport.ts | 29 +++++++ packages/iconer/src/vite-env.d.ts | 1 + packages/iconer/tsconfig.app.json | 40 ++++++++++ packages/iconer/tsconfig.json | 11 +++ packages/iconer/tsconfig.node.json | 16 ++++ .../iconer/types/src/components/svg-icon.d.ts | 9 +++ .../types/src/generated/icon-names.d.ts | 1 + packages/iconer/types/src/index.d.ts | 3 + .../types/src/utils/useLazySvgImport.d.ts | 6 ++ packages/iconer/vite.config.ts | 63 +++++++++++++++ 67 files changed, 649 insertions(+), 8 deletions(-) create mode 100644 apps/server/src/queue/general/general-queue.listener.ts create mode 100644 apps/server/src/queue/general/general-queue.service.ts create mode 100644 apps/server/src/queue/general/general.service.ts create mode 100644 apps/server/src/queue/queue.module.ts create mode 100644 apps/server/src/queue/worker/processor.ts create mode 100644 apps/server/src/socket/socket.gateway.spec.ts create mode 100644 apps/server/src/socket/socket.gateway.ts create mode 100644 apps/server/src/utils/tusd.ts mode change 100644 => 100755 packages/common/.env.example mode change 100644 => 100755 packages/common/package.json mode change 100644 => 100755 packages/common/prisma/schema.prisma mode change 100644 => 100755 packages/common/src/index.ts mode change 100644 => 100755 packages/common/src/schema.ts mode change 100644 => 100755 packages/common/tsconfig.cjs.json mode change 100644 => 100755 packages/common/tsconfig.esm.json create mode 100755 packages/iconer/.eslintrc.cjs create mode 100755 packages/iconer/README.md create mode 100755 packages/iconer/package.json create mode 100755 packages/iconer/public/vite.svg create mode 100755 packages/iconer/src/components/svg-icon.tsx create mode 100644 packages/iconer/src/generated/icon-names.ts create mode 100644 packages/iconer/src/icons/align-center.svg create mode 100644 packages/iconer/src/icons/align-justify.svg create mode 100644 packages/iconer/src/icons/align-left.svg create mode 100644 packages/iconer/src/icons/align-right.svg create mode 100644 packages/iconer/src/icons/arrow-drop-down.svg create mode 100644 packages/iconer/src/icons/bold.svg create mode 100644 packages/iconer/src/icons/check.svg create mode 100644 packages/iconer/src/icons/content.svg create mode 100644 packages/iconer/src/icons/copy.svg create mode 100644 packages/iconer/src/icons/edit.svg create mode 100644 packages/iconer/src/icons/get-text.svg create mode 100644 packages/iconer/src/icons/home.svg create mode 100644 packages/iconer/src/icons/horizontal-rule.svg create mode 100644 packages/iconer/src/icons/image.svg create mode 100644 packages/iconer/src/icons/italic.svg create mode 100644 packages/iconer/src/icons/link-off.svg create mode 100644 packages/iconer/src/icons/link.svg create mode 100644 packages/iconer/src/icons/logout.svg create mode 100755 packages/iconer/src/icons/react.svg create mode 100644 packages/iconer/src/icons/redo.svg create mode 100644 packages/iconer/src/icons/share.svg create mode 100644 packages/iconer/src/icons/strike.svg create mode 100644 packages/iconer/src/icons/text-indent.svg create mode 100644 packages/iconer/src/icons/text-outdent.svg create mode 100644 packages/iconer/src/icons/underline.svg create mode 100644 packages/iconer/src/icons/undo.svg create mode 100644 packages/iconer/src/icons/zoomin.svg create mode 100644 packages/iconer/src/icons/zoomout.svg create mode 100755 packages/iconer/src/index.css create mode 100755 packages/iconer/src/index.ts create mode 100644 packages/iconer/src/utils/useLazySvgImport.ts create mode 100755 packages/iconer/src/vite-env.d.ts create mode 100755 packages/iconer/tsconfig.app.json create mode 100755 packages/iconer/tsconfig.json create mode 100755 packages/iconer/tsconfig.node.json create mode 100644 packages/iconer/types/src/components/svg-icon.d.ts create mode 100644 packages/iconer/types/src/generated/icon-names.d.ts create mode 100644 packages/iconer/types/src/index.d.ts create mode 100644 packages/iconer/types/src/utils/useLazySvgImport.d.ts create mode 100755 packages/iconer/vite.config.ts diff --git a/apps/server/.env.example b/apps/server/.env.example index e69de29..785f80b 100644 --- a/apps/server/.env.example +++ b/apps/server/.env.example @@ -0,0 +1,10 @@ +DATABASE_URL="postgresql://root:Letusdoit000@localhost:5432/mydb?schema=public" +REDIS_HOST=localhost +REDIS_PORT=6379 +# .env +ELASTICSEARCH_NODE=http://localhost:9200 +ELASTICSEARCH_USERNAME=elastic +ELASTICSEARCH_PASSWORD=RXoH-DI1lgJB-hY0*pLk +TUS_URL=http://localhost:8080 +PYTHON_URL=http://localhost:8000 +APP_URL=http://localhost:5173 \ No newline at end of file diff --git a/apps/server/package.json b/apps/server/package.json index 3e448e7..0198d66 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -20,15 +20,23 @@ "test:e2e": "jest --config ./test/jest-e2e.json" }, "dependencies": { + "@nestjs/bullmq": "^10.2.0", "@nestjs/common": "^10.3.10", + "@nestjs/config": "^3.2.3", "@nestjs/core": "^10.0.0", "@nestjs/platform-express": "^10.0.0", + "@nestjs/websockets": "^10.3.10", "@nicestack/common": "workspace:^", "@trpc/server": "11.0.0-rc.456", + "axios": "^1.7.3", + "bullmq": "^5.12.0", "ioredis": "^5.4.1", + "mime-types": "^2.1.35", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", + "socket.io": "^4.7.5", "superjson-cjs": "^2.2.3", + "tus-js-client": "^4.1.0", "zod": "^3.23.8" }, "devDependencies": { @@ -37,6 +45,7 @@ "@nestjs/testing": "^10.0.0", "@types/express": "^4.17.17", "@types/jest": "^29.5.2", + "@types/mime-types": "^2.1.4", "@types/node": "^20.3.1", "@types/supertest": "^6.0.0", "@typescript-eslint/eslint-plugin": "^6.0.0", diff --git a/apps/server/src/app.module.ts b/apps/server/src/app.module.ts index cd35472..3e1647d 100644 --- a/apps/server/src/app.module.ts +++ b/apps/server/src/app.module.ts @@ -5,10 +5,12 @@ import { TrpcModule } from './trpc/trpc.module'; import { RedisService } from './redis/redis.service'; import { RedisModule } from './redis/redis.module'; +import { SocketGateway } from './socket/socket.gateway'; +import { QueueModule } from './queue/queue.module'; @Module({ - imports: [TrpcModule, RedisModule], + imports: [TrpcModule, RedisModule, QueueModule], controllers: [AppController], - providers: [AppService, RedisService], + providers: [AppService, RedisService, SocketGateway], }) export class AppModule { } diff --git a/apps/server/src/main.ts b/apps/server/src/main.ts index c99c6da..f1f0f2a 100644 --- a/apps/server/src/main.ts +++ b/apps/server/src/main.ts @@ -4,7 +4,9 @@ import { TrpcRouter } from './trpc/trpc.router'; async function bootstrap() { const app = await NestFactory.create(AppModule); - app.enableCors(); + app.enableCors({ + origin: [process.env.APP_URL!], + }); const trpc = app.get(TrpcRouter); trpc.applyMiddleware(app); await app.listen(3000); diff --git a/apps/server/src/queue/general/general-queue.listener.ts b/apps/server/src/queue/general/general-queue.listener.ts new file mode 100644 index 0000000..e31328d --- /dev/null +++ b/apps/server/src/queue/general/general-queue.listener.ts @@ -0,0 +1,32 @@ +import { + QueueEventsListener, + QueueEventsHost, + OnQueueEvent, + InjectQueue, +} from '@nestjs/bullmq'; +import { SocketGateway } from '@server/socket/socket.gateway'; +import { Queue } from 'bullmq'; + + +@QueueEventsListener('general') +export class GeneralQueueEvents extends QueueEventsHost { + constructor(@InjectQueue('general') private generalQueue: Queue, private socketGateway: SocketGateway) { + super() + } + + @OnQueueEvent('completed') + async onCompleted({ + jobId, + returnvalue + }: { + jobId: string; + returnvalue: string; + prev?: string; + }) { + + } + @OnQueueEvent("progress") + async onProgress({ jobId, data }: { jobId: string, data: any }) { + + } +} \ No newline at end of file diff --git a/apps/server/src/queue/general/general-queue.service.ts b/apps/server/src/queue/general/general-queue.service.ts new file mode 100644 index 0000000..e5ba137 --- /dev/null +++ b/apps/server/src/queue/general/general-queue.service.ts @@ -0,0 +1,23 @@ +import { InjectQueue } from '@nestjs/bullmq'; +import { Injectable, Logger, OnModuleInit } from '@nestjs/common'; +import { Job, Queue } from 'bullmq'; +import { SocketGateway } from '@server/socket/socket.gateway'; +@Injectable() +export class GeneralQueueService implements OnModuleInit { + private readonly logger = new Logger(GeneralQueueService.name,) + constructor(@InjectQueue('general') private generalQueue: Queue, private socketGateway: SocketGateway) { } + onModuleInit() { + this.logger.log(`general queue service init at pid=${process.pid}`) + + } + async addJob(data: any) { + this.logger.log('add embedding job', data.title) + + await this.generalQueue.add('embedding', data, { debounce: { id: data.id } }); + + } + async getWaitingJobs() { + const waitingJobs = await this.generalQueue.getJobs(["waiting"]) + return waitingJobs + } +} diff --git a/apps/server/src/queue/general/general.service.ts b/apps/server/src/queue/general/general.service.ts new file mode 100644 index 0000000..6d44fab --- /dev/null +++ b/apps/server/src/queue/general/general.service.ts @@ -0,0 +1,17 @@ +import axios, { AxiosInstance } from 'axios'; +import { Injectable, Logger } from '@nestjs/common'; + +@Injectable() +export class GeneralService { + private axiosInstance: AxiosInstance; + private logger: Logger; + + constructor() { + const PYTHON_ENDPOINT = process.env.PYTHON_URL || 'http://localhost:8000'; + this.logger = new Logger(GeneralService.name); + this.axiosInstance = axios.create({ + baseURL: PYTHON_ENDPOINT, + timeout: 120000, // 设置请求超时时间 + }); + } +} diff --git a/apps/server/src/queue/queue.module.ts b/apps/server/src/queue/queue.module.ts new file mode 100644 index 0000000..4b38394 --- /dev/null +++ b/apps/server/src/queue/queue.module.ts @@ -0,0 +1,22 @@ +import { BullModule } from '@nestjs/bullmq'; +import { Logger, Module } from '@nestjs/common'; + +import { join } from 'path'; +import { SocketGateway } from '@server/socket/socket.gateway'; + +@Module({ + imports: [ + BullModule.forRoot({ + connection: { + host: 'localhost', + port: 6379, + }, + }), BullModule.registerQueue({ + name: 'general', + processors: [join(__dirname, 'worker/processor.js')], + }) + ], + providers: [Logger, SocketGateway], + exports: [] +}) +export class QueueModule { } diff --git a/apps/server/src/queue/worker/processor.ts b/apps/server/src/queue/worker/processor.ts new file mode 100644 index 0000000..9f1ef68 --- /dev/null +++ b/apps/server/src/queue/worker/processor.ts @@ -0,0 +1,5 @@ +import { Job } from 'bullmq'; +export default async function (job: Job) { + + +} \ No newline at end of file diff --git a/apps/server/src/redis/redis.module.ts b/apps/server/src/redis/redis.module.ts index 3558eec..f5759a9 100644 --- a/apps/server/src/redis/redis.module.ts +++ b/apps/server/src/redis/redis.module.ts @@ -1,9 +1,10 @@ // redis.module.ts import { Module } from '@nestjs/common'; import { RedisService } from './redis.service'; +import { ConfigService } from '@nestjs/config'; @Module({ - providers: [RedisService], // 注册 RedisService 作为提供者 + providers: [RedisService, ConfigService], // 注册 RedisService 作为提供者 exports: [RedisService], // 导出 RedisService }) diff --git a/apps/server/src/redis/redis.service.ts b/apps/server/src/redis/redis.service.ts index 7b12de6..efc0bc6 100644 --- a/apps/server/src/redis/redis.service.ts +++ b/apps/server/src/redis/redis.service.ts @@ -1,23 +1,73 @@ import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; import Redis from 'ioredis'; @Injectable() export class RedisService { private readonly redisClient: Redis; - constructor() { + constructor(private readonly configService: ConfigService) { this.redisClient = new Redis({ - host: process.env.REDIS_HOST, - port: parseInt(process.env.REDIS_PORT!), // Redis 服务器的端口 + host: configService.get('REDIS_HOST'), + port: configService.get('REDIS_PORT'), // Redis 服务器的端口 }); } setValue(key: string, value: string) { return this.redisClient.set(key, value); } - getValue(key: string) { return this.redisClient.get(key); + + } + keys(pattern: string) { + return this.redisClient.keys(pattern) + } + setWithExpiry(key: string, value: string, time: number) { + return this.redisClient.setex(key, time, value); + } + deleteKey(key: string) { + return this.redisClient.del(key); + } + setHashField(key: string, field: string, value: string) { + return this.redisClient.hset(key, field, value); + } + //获取key中的field字段数据 + getHashField(key: string, field: string) { + return this.redisClient.hget(key, field); + } + //获取key中所有数据 + getAllHashFields(key: string) { + return this.redisClient.hgetall(key); + } + publishMessage(channel: string, message: string) { + return this.redisClient.publish(channel, message); + } + + // 订阅消息,需要提供一个回调函数来处理接收到的消息 + subscribeToMessages(channel: string, messageHandler: (channel: string, message: string) => void) { + this.redisClient.subscribe(channel, (err, count) => { + if (err) { + console.error('Subscription error', err); + } else { + console.log(`Subscribed to ${count} channels`); + } + }); + + this.redisClient.on('message', (channel, message) => { + console.log(`Received message ${message} from channel ${channel}`); + messageHandler(channel, message); + }); + } + + // 取消订阅指定的频道 + unsubscribeFromChannel(channel: string) { + return this.redisClient.unsubscribe(channel); + } + + // 取消订阅所有频道 + unsubscribeAll() { + return this.redisClient.quit(); } } diff --git a/apps/server/src/socket/socket.gateway.spec.ts b/apps/server/src/socket/socket.gateway.spec.ts new file mode 100644 index 0000000..8cf8bf5 --- /dev/null +++ b/apps/server/src/socket/socket.gateway.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { SocketGateway } from './socket.gateway'; + +describe('SocketGateway', () => { + let gateway: SocketGateway; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [SocketGateway], + }).compile(); + + gateway = module.get(SocketGateway); + }); + + it('should be defined', () => { + expect(gateway).toBeDefined(); + }); +}); diff --git a/apps/server/src/socket/socket.gateway.ts b/apps/server/src/socket/socket.gateway.ts new file mode 100644 index 0000000..a84f182 --- /dev/null +++ b/apps/server/src/socket/socket.gateway.ts @@ -0,0 +1,28 @@ +import { WebSocketGateway, WebSocketServer, OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect } from '@nestjs/websockets'; +import { Server, Socket } from 'socket.io'; + +@WebSocketGateway(3001, { + namespace: 'library-events', + cors: { + origin: '*', // 或者你可以指定特定的来源,例如 "http://localhost:3000" + methods: ['GET', 'POST'], + credentials: true + } +}) +export class SocketGateway implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect { + + @WebSocketServer() server: Server; + + afterInit(server: Server) { + console.log('WebSocket initialized'); + } + + handleConnection(client: Socket, ...args: any[]) { + console.log(`Client connected: ${client.id}`); + } + + handleDisconnect(client: Socket) { + console.log(`Client disconnected: ${client.id}`); + } + +} diff --git a/apps/server/src/utils/tusd.ts b/apps/server/src/utils/tusd.ts new file mode 100644 index 0000000..be73fed --- /dev/null +++ b/apps/server/src/utils/tusd.ts @@ -0,0 +1,76 @@ +import * as tus from 'tus-js-client'; +import { promises as fs } from 'fs'; +import * as mime from 'mime-types'; + +export const uploader = async ( + endpoint: string = 'http://localhost:8080', + input: Buffer | string, + externalFileName: string = 'unknown', // 允许外部传入文件名 + onProgress?: (percentage: number) => void, + onSuccess?: (url: string) => void, + onError?: (error: Error) => void +) => { + let fileBuffer: Buffer; + let fileName: string; + let fileType: string; + + // 确定输入是Buffer还是文件路径 + if (typeof input === 'string') { + try { + fileBuffer = await fs.readFile(input); + fileName = input.split('/').pop() || 'unknown'; + fileType = mime.lookup(input) || 'application/octet-stream'; + } catch (error: any) { + console.error("读取文件失败: " + error.message); + if (onError) onError(error); + return; + } + } else { + fileBuffer = input; + fileName = externalFileName; // 使用外部传入的文件名 + // 尝试获取文件类型,这里简化处理,实际应用中可能需要更复杂的逻辑 + fileType = mime.lookup(fileName) || 'application/octet-stream'; + } + + const upload = new tus.Upload(fileBuffer as any, { + endpoint: `${endpoint}/files/`, + retryDelays: [0, 3000, 5000, 10000, 20000], + metadata: { filename: fileName, filetype: fileType }, + onError: (error) => { + console.error("上传失败: " + error.message); + if (onError) onError(error); + }, + onProgress: (bytesUploaded, bytesTotal) => { + const percentage = (bytesUploaded / bytesTotal) * 100; + if (onProgress) onProgress(percentage); + }, + onSuccess: () => { + console.log("上传完成"); + if (onSuccess) onSuccess(upload.url!); + }, + }); + + // 寻找并继续之前的上传 + upload.findPreviousUploads().then((previousUploads) => { + if (previousUploads && previousUploads.length > 0) { + upload.resumeFromPreviousUpload(previousUploads[0]!); + } + }); + + return upload; +}; + +export const uploaderPromise = ( + endpoint: string, + input: Buffer | string, + externalFileName: string = 'unknown', // 允许外部传入文件名 + onProgress?: (percentage: number) => void +): Promise => { + return new Promise((resolve, reject) => { + uploader(endpoint, input, externalFileName, onProgress, resolve, reject) + .then((upload) => { + upload!.start(); + }) + .catch(reject); + }); +}; \ No newline at end of file diff --git a/packages/common/.env.example b/packages/common/.env.example old mode 100644 new mode 100755 diff --git a/packages/common/package.json b/packages/common/package.json old mode 100644 new mode 100755 diff --git a/packages/common/prisma/schema.prisma b/packages/common/prisma/schema.prisma old mode 100644 new mode 100755 diff --git a/packages/common/src/index.ts b/packages/common/src/index.ts old mode 100644 new mode 100755 diff --git a/packages/common/src/schema.ts b/packages/common/src/schema.ts old mode 100644 new mode 100755 diff --git a/packages/common/tsconfig.cjs.json b/packages/common/tsconfig.cjs.json old mode 100644 new mode 100755 diff --git a/packages/common/tsconfig.esm.json b/packages/common/tsconfig.esm.json old mode 100644 new mode 100755 diff --git a/packages/iconer/.eslintrc.cjs b/packages/iconer/.eslintrc.cjs new file mode 100755 index 0000000..d6c9537 --- /dev/null +++ b/packages/iconer/.eslintrc.cjs @@ -0,0 +1,18 @@ +module.exports = { + root: true, + env: { browser: true, es2020: true }, + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:react-hooks/recommended', + ], + ignorePatterns: ['dist', '.eslintrc.cjs'], + parser: '@typescript-eslint/parser', + plugins: ['react-refresh'], + rules: { + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + }, +} diff --git a/packages/iconer/README.md b/packages/iconer/README.md new file mode 100755 index 0000000..e1cdc89 --- /dev/null +++ b/packages/iconer/README.md @@ -0,0 +1,30 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type aware lint rules: + +- Configure the top-level `parserOptions` property like this: + +```js +export default { + // other rules... + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + project: ['./tsconfig.json', './tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: __dirname, + }, +} +``` + +- Replace `plugin:@typescript-eslint/recommended` to `plugin:@typescript-eslint/recommended-type-checked` or `plugin:@typescript-eslint/strict-type-checked` +- Optionally add `plugin:@typescript-eslint/stylistic-type-checked` +- Install [eslint-plugin-react](https://github.com/jsx-eslint/eslint-plugin-react) and add `plugin:react/recommended` & `plugin:react/jsx-runtime` to the `extends` list diff --git a/packages/iconer/package.json b/packages/iconer/package.json new file mode 100755 index 0000000..88ef681 --- /dev/null +++ b/packages/iconer/package.json @@ -0,0 +1,33 @@ +{ + "name": "@nicestack/iconer", + "private": true, + "version": "0.0.0", + "type": "module", + "main": "./dist/index.umd.js", + "module": "./dist/index.es.js", + "types": "./types/src/index.d.ts", + "scripts": { + "dev": "pnpm build && concurrently \"chokidar ./src -c 'pnpm build' -i '**/generated/**'\"", + "build": "tsc -b && vite build", + "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", + "preview": "vite preview" + }, + "dependencies": {}, + "devDependencies": { + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@typescript-eslint/eslint-plugin": "^7.15.0", + "@typescript-eslint/parser": "^7.15.0", + "@vitejs/plugin-react": "^4.3.1", + "eslint": "^8.57.0", + "eslint-plugin-react-hooks": "^4.6.2", + "eslint-plugin-react-refresh": "^0.4.7", + "typescript": "^5.2.2", + "vite": "^5.3.4", + "vite-plugin-svgr": "^4.2.0", + "concurrently": "^8.0.1", + "chokidar-cli": "^3.0.0", + "react": "^18.3.1", + "react-dom": "^18.3.1" + } +} \ No newline at end of file diff --git a/packages/iconer/public/vite.svg b/packages/iconer/public/vite.svg new file mode 100755 index 0000000..e7b8dfb --- /dev/null +++ b/packages/iconer/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/iconer/src/components/svg-icon.tsx b/packages/iconer/src/components/svg-icon.tsx new file mode 100755 index 0000000..bbb8820 --- /dev/null +++ b/packages/iconer/src/components/svg-icon.tsx @@ -0,0 +1,40 @@ +import { IconName } from "../generated/icon-names"; +import { useLazySvgImport } from "../utils/useLazySvgImport"; + +interface IProps { + name: IconName; + className?: string, + svgProp?: React.SVGProps; + size?: 'small' | 'middle' | 'large' | number; +} +const sizeMap: { [key: string]: number } = { + small: 16, + middle: 24, + large: 32 +}; +function Icon(props: IProps) { + const { name, svgProp, className = '', size = 'middle' } = props; + const { loading, Svg } = useLazySvgImport(name); + const finalSize = typeof size === 'number' ? size : sizeMap[size] || sizeMap.middle; + const svgStyle = { + width: finalSize, + height: finalSize, + ...(svgProp?.style) // 如果svgProp中包含style,则合并样式 + }; + return ( + <> + {loading && ( +
+ )} + {Svg && ( + + )} + + ); +} + +export default Icon; \ No newline at end of file diff --git a/packages/iconer/src/generated/icon-names.ts b/packages/iconer/src/generated/icon-names.ts new file mode 100644 index 0000000..46915e0 --- /dev/null +++ b/packages/iconer/src/generated/icon-names.ts @@ -0,0 +1,2 @@ +export type IconName = 'align-center' | 'align-justify' | 'align-left' | 'align-right' | 'arrow-drop-down' | 'bold' | 'check' | 'content' | 'copy' | 'edit' | 'get-text' | 'home' | 'horizontal-rule' | 'image' | 'italic' | 'link-off' | 'link' | 'logout' | 'react' | 'redo' | 'share' | 'strike' | 'text-indent' | 'text-outdent' | 'underline' | 'undo' | 'zoomin' | 'zoomout' + \ No newline at end of file diff --git a/packages/iconer/src/icons/align-center.svg b/packages/iconer/src/icons/align-center.svg new file mode 100644 index 0000000..f8f0664 --- /dev/null +++ b/packages/iconer/src/icons/align-center.svg @@ -0,0 +1,4 @@ + + + \ No newline at end of file diff --git a/packages/iconer/src/icons/align-justify.svg b/packages/iconer/src/icons/align-justify.svg new file mode 100644 index 0000000..a24ab65 --- /dev/null +++ b/packages/iconer/src/icons/align-justify.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/iconer/src/icons/align-left.svg b/packages/iconer/src/icons/align-left.svg new file mode 100644 index 0000000..71173e3 --- /dev/null +++ b/packages/iconer/src/icons/align-left.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/iconer/src/icons/align-right.svg b/packages/iconer/src/icons/align-right.svg new file mode 100644 index 0000000..0768c1e --- /dev/null +++ b/packages/iconer/src/icons/align-right.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/iconer/src/icons/arrow-drop-down.svg b/packages/iconer/src/icons/arrow-drop-down.svg new file mode 100644 index 0000000..96b1813 --- /dev/null +++ b/packages/iconer/src/icons/arrow-drop-down.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/iconer/src/icons/bold.svg b/packages/iconer/src/icons/bold.svg new file mode 100644 index 0000000..6f006dd --- /dev/null +++ b/packages/iconer/src/icons/bold.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/iconer/src/icons/check.svg b/packages/iconer/src/icons/check.svg new file mode 100644 index 0000000..13fbf39 --- /dev/null +++ b/packages/iconer/src/icons/check.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/iconer/src/icons/content.svg b/packages/iconer/src/icons/content.svg new file mode 100644 index 0000000..b206047 --- /dev/null +++ b/packages/iconer/src/icons/content.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/iconer/src/icons/copy.svg b/packages/iconer/src/icons/copy.svg new file mode 100644 index 0000000..3e6a041 --- /dev/null +++ b/packages/iconer/src/icons/copy.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/iconer/src/icons/edit.svg b/packages/iconer/src/icons/edit.svg new file mode 100644 index 0000000..073e6bd --- /dev/null +++ b/packages/iconer/src/icons/edit.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/iconer/src/icons/get-text.svg b/packages/iconer/src/icons/get-text.svg new file mode 100644 index 0000000..232af79 --- /dev/null +++ b/packages/iconer/src/icons/get-text.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/iconer/src/icons/home.svg b/packages/iconer/src/icons/home.svg new file mode 100644 index 0000000..edee4db --- /dev/null +++ b/packages/iconer/src/icons/home.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/iconer/src/icons/horizontal-rule.svg b/packages/iconer/src/icons/horizontal-rule.svg new file mode 100644 index 0000000..eaa1339 --- /dev/null +++ b/packages/iconer/src/icons/horizontal-rule.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/iconer/src/icons/image.svg b/packages/iconer/src/icons/image.svg new file mode 100644 index 0000000..08b52fb --- /dev/null +++ b/packages/iconer/src/icons/image.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/iconer/src/icons/italic.svg b/packages/iconer/src/icons/italic.svg new file mode 100644 index 0000000..e2de02b --- /dev/null +++ b/packages/iconer/src/icons/italic.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/iconer/src/icons/link-off.svg b/packages/iconer/src/icons/link-off.svg new file mode 100644 index 0000000..68013c4 --- /dev/null +++ b/packages/iconer/src/icons/link-off.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/iconer/src/icons/link.svg b/packages/iconer/src/icons/link.svg new file mode 100644 index 0000000..7229f2f --- /dev/null +++ b/packages/iconer/src/icons/link.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/iconer/src/icons/logout.svg b/packages/iconer/src/icons/logout.svg new file mode 100644 index 0000000..26aad05 --- /dev/null +++ b/packages/iconer/src/icons/logout.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/iconer/src/icons/react.svg b/packages/iconer/src/icons/react.svg new file mode 100755 index 0000000..ec80c06 --- /dev/null +++ b/packages/iconer/src/icons/react.svg @@ -0,0 +1,6 @@ + \ No newline at end of file diff --git a/packages/iconer/src/icons/redo.svg b/packages/iconer/src/icons/redo.svg new file mode 100644 index 0000000..0d922ea --- /dev/null +++ b/packages/iconer/src/icons/redo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/iconer/src/icons/share.svg b/packages/iconer/src/icons/share.svg new file mode 100644 index 0000000..56f6e20 --- /dev/null +++ b/packages/iconer/src/icons/share.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/iconer/src/icons/strike.svg b/packages/iconer/src/icons/strike.svg new file mode 100644 index 0000000..1c8e3b6 --- /dev/null +++ b/packages/iconer/src/icons/strike.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/iconer/src/icons/text-indent.svg b/packages/iconer/src/icons/text-indent.svg new file mode 100644 index 0000000..b7b6a30 --- /dev/null +++ b/packages/iconer/src/icons/text-indent.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/iconer/src/icons/text-outdent.svg b/packages/iconer/src/icons/text-outdent.svg new file mode 100644 index 0000000..c5141cb --- /dev/null +++ b/packages/iconer/src/icons/text-outdent.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/iconer/src/icons/underline.svg b/packages/iconer/src/icons/underline.svg new file mode 100644 index 0000000..0f9d75d --- /dev/null +++ b/packages/iconer/src/icons/underline.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/iconer/src/icons/undo.svg b/packages/iconer/src/icons/undo.svg new file mode 100644 index 0000000..6e568c3 --- /dev/null +++ b/packages/iconer/src/icons/undo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/iconer/src/icons/zoomin.svg b/packages/iconer/src/icons/zoomin.svg new file mode 100644 index 0000000..f2b58f1 --- /dev/null +++ b/packages/iconer/src/icons/zoomin.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/iconer/src/icons/zoomout.svg b/packages/iconer/src/icons/zoomout.svg new file mode 100644 index 0000000..259f33a --- /dev/null +++ b/packages/iconer/src/icons/zoomout.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/iconer/src/index.css b/packages/iconer/src/index.css new file mode 100755 index 0000000..e69de29 diff --git a/packages/iconer/src/index.ts b/packages/iconer/src/index.ts new file mode 100755 index 0000000..5d9afd7 --- /dev/null +++ b/packages/iconer/src/index.ts @@ -0,0 +1,7 @@ +// import React from 'react' +// import ReactDOM from 'react-dom/client' +// import App from './App.tsx' +import './index.css' +import Icon from "./components/svg-icon" + +export { Icon } diff --git a/packages/iconer/src/utils/useLazySvgImport.ts b/packages/iconer/src/utils/useLazySvgImport.ts new file mode 100644 index 0000000..c9f8749 --- /dev/null +++ b/packages/iconer/src/utils/useLazySvgImport.ts @@ -0,0 +1,29 @@ +import { ComponentProps, FC, useEffect, useRef, useState } from "react"; + +export const useLazySvgImport = (name: string) => { + const importRef = useRef>>(); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(); + + useEffect(() => { + setLoading(true); + const importIcon = async () => { + try { + importRef.current = ( + await import(`../icons/${name}.svg?react`) + ).default; // We use `?react` here following `vite-plugin-svgr`'s convention. + } catch (err) { + setError(err as Error); + } finally { + setLoading(false); + } + }; + importIcon(); + }, [name]); + + return { + error, + loading, + Svg: importRef.current, + }; +}; \ No newline at end of file diff --git a/packages/iconer/src/vite-env.d.ts b/packages/iconer/src/vite-env.d.ts new file mode 100755 index 0000000..11f02fe --- /dev/null +++ b/packages/iconer/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/packages/iconer/tsconfig.app.json b/packages/iconer/tsconfig.app.json new file mode 100755 index 0000000..a9eb8c8 --- /dev/null +++ b/packages/iconer/tsconfig.app.json @@ -0,0 +1,40 @@ +{ + "compilerOptions": { + "composite": true, + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2020", + "useDefineForClassFields": true, + "lib": [ + "ES2020", + "DOM", + "DOM.Iterable" + ], + "module": "ESNext", + "skipLibCheck": true, + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "allowSyntheticDefaultImports": true, + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "react-jsx", + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "declaration": true, + "declarationDir": "types", + "emitDeclarationOnly": true, + "types": [ + "vite-plugin-svgr/client" + ] + }, + "include": [ + "src" + ], + "exclude": [ + "node_modules", + "dist" + ] +} \ No newline at end of file diff --git a/packages/iconer/tsconfig.json b/packages/iconer/tsconfig.json new file mode 100755 index 0000000..65f670c --- /dev/null +++ b/packages/iconer/tsconfig.json @@ -0,0 +1,11 @@ +{ + "files": [], + "references": [ + { + "path": "./tsconfig.app.json" + }, + { + "path": "./tsconfig.node.json" + } + ] +} \ No newline at end of file diff --git a/packages/iconer/tsconfig.node.json b/packages/iconer/tsconfig.node.json new file mode 100755 index 0000000..0be0958 --- /dev/null +++ b/packages/iconer/tsconfig.node.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "composite": true, + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "skipLibCheck": true, + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true, + "strict": true, + "noEmit": true + }, + "include": [ + "vite.config.ts", + + ] +} \ No newline at end of file diff --git a/packages/iconer/types/src/components/svg-icon.d.ts b/packages/iconer/types/src/components/svg-icon.d.ts new file mode 100644 index 0000000..da84d79 --- /dev/null +++ b/packages/iconer/types/src/components/svg-icon.d.ts @@ -0,0 +1,9 @@ +import { IconName } from "../generated/icon-names"; +interface IProps { + name: IconName; + className?: string; + svgProp?: React.SVGProps; + size?: 'small' | 'middle' | 'large' | number; +} +declare function Icon(props: IProps): import("react/jsx-runtime").JSX.Element; +export default Icon; diff --git a/packages/iconer/types/src/generated/icon-names.d.ts b/packages/iconer/types/src/generated/icon-names.d.ts new file mode 100644 index 0000000..1df58f0 --- /dev/null +++ b/packages/iconer/types/src/generated/icon-names.d.ts @@ -0,0 +1 @@ +export type IconName = 'align-center' | 'align-justify' | 'align-left' | 'align-right' | 'arrow-drop-down' | 'bold' | 'check' | 'content' | 'copy' | 'edit' | 'get-text' | 'home' | 'horizontal-rule' | 'image' | 'italic' | 'link-off' | 'link' | 'logout' | 'react' | 'redo' | 'share' | 'strike' | 'text-indent' | 'text-outdent' | 'underline' | 'undo' | 'zoomin' | 'zoomout'; diff --git a/packages/iconer/types/src/index.d.ts b/packages/iconer/types/src/index.d.ts new file mode 100644 index 0000000..d8280d4 --- /dev/null +++ b/packages/iconer/types/src/index.d.ts @@ -0,0 +1,3 @@ +import './index.css'; +import Icon from "./components/svg-icon"; +export { Icon }; diff --git a/packages/iconer/types/src/utils/useLazySvgImport.d.ts b/packages/iconer/types/src/utils/useLazySvgImport.d.ts new file mode 100644 index 0000000..b40b5c8 --- /dev/null +++ b/packages/iconer/types/src/utils/useLazySvgImport.d.ts @@ -0,0 +1,6 @@ +import { FC } from "react"; +export declare const useLazySvgImport: (name: string) => { + error: Error | undefined; + loading: boolean; + Svg: FC> | undefined; +}; diff --git a/packages/iconer/vite.config.ts b/packages/iconer/vite.config.ts new file mode 100755 index 0000000..2a85d39 --- /dev/null +++ b/packages/iconer/vite.config.ts @@ -0,0 +1,63 @@ +import { defineConfig } from 'vite' +import react from '@vitejs/plugin-react' +import svgr from "vite-plugin-svgr"; +import path from 'path'; +import fs from 'fs'; +function generateIconTypes() { + const iconsDir = path.resolve('src/icons'); + const outputPath = path.resolve('src/generated/icon-names.ts'); + + return { + name: 'generate-icon-types', + buildStart() { + // Check if the icons directory exists + if (!fs.existsSync(iconsDir)) { + console.log(`Directory ${iconsDir} does not exist. Skipping icon type generation.`); + return; + } + + // Read the icons directory + const files = fs.readdirSync(iconsDir); + + // Filter out non-SVG files and get just the base names without extension + const iconNames = files + .filter(file => file.endsWith('.svg')) + .map(file => path.basename(file, '.svg')); + + // Create type definition string + const typeDefinitions = `export type IconName = ${iconNames.map(name => `'${name}'`).join(' | ')} + `; + + // Write the type definitions to the output file + fs.writeFileSync(outputPath, typeDefinitions); + } + }; +} + + +// 在 UMD 构建模式下为外部依赖提供一个全局变量 +export const GLOBALS = { + react: 'React', + 'react-dom': 'ReactDOM', +}; +// 处理类库使用到的外部依赖 +// 确保外部化处理那些你不想打包进库的依赖 +export const EXTERNAL = [ + 'react', + 'react-dom', +]; +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react(), svgr(), generateIconTypes()], + build: { + rollupOptions: { + external: EXTERNAL, + output: { globals: GLOBALS }, + }, + lib: { + entry: path.resolve(__dirname, 'src/index.ts'), + name: 'iconer', + fileName: (format) => `index.${format}.js`, + } + }, +})