From 6b651fc6126bf610f2ac22023d0baad97af6cfe2 Mon Sep 17 00:00:00 2001 From: sunlei Date: Wed, 3 Jun 2026 16:07:41 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=8F=91=E5=B8=83=E8=87=AA=E7=A0=94=20?= =?UTF-8?q?Knife4j=20=E9=80=82=E9=85=8D=E5=8C=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 + .husky/commit-msg | 1 + .husky/pre-commit | 1 + apps/playground/src/main.tsx | 88 ++++++----- apps/playground/vite.config.ts | 5 + package.json | 9 +- .../src/components/Knife4jApiPanel.tsx | 2 - packages/nestjs-knife4j/package.json | 27 ++++ packages/nestjs-knife4j/scripts/copy-ui.mjs | 23 +++ packages/nestjs-knife4j/src/index.ts | 139 ++++++++++++++++++ packages/nestjs-knife4j/tsconfig.json | 14 ++ pnpm-lock.yaml | 126 ++++++++++++++-- scripts/validate-commit-msg.mjs | 24 +++ tsconfig.base.json | 4 +- tsconfig.json | 3 + 15 files changed, 416 insertions(+), 52 deletions(-) create mode 100644 .husky/commit-msg create mode 100644 .husky/pre-commit create mode 100644 packages/nestjs-knife4j/package.json create mode 100644 packages/nestjs-knife4j/scripts/copy-ui.mjs create mode 100644 packages/nestjs-knife4j/src/index.ts create mode 100644 packages/nestjs-knife4j/tsconfig.json create mode 100644 scripts/validate-commit-msg.mjs diff --git a/.gitignore b/.gitignore index a4fd36b..9210d75 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,5 @@ dist **/.turbo *.log *.tsbuildinfo +*.tgz +packages/nestjs-knife4j/ui diff --git a/.husky/commit-msg b/.husky/commit-msg new file mode 100644 index 0000000..5c056bb --- /dev/null +++ b/.husky/commit-msg @@ -0,0 +1 @@ +node scripts/validate-commit-msg.mjs "$1" diff --git a/.husky/pre-commit b/.husky/pre-commit new file mode 100644 index 0000000..8ff2704 --- /dev/null +++ b/.husky/pre-commit @@ -0,0 +1 @@ +pnpm run verify:commit diff --git a/apps/playground/src/main.tsx b/apps/playground/src/main.tsx index cd4027f..bfd02b2 100644 --- a/apps/playground/src/main.tsx +++ b/apps/playground/src/main.tsx @@ -3,7 +3,13 @@ import { createRouter, createWebHashHistory, RouterView } from 'vue-router'; import { Knife4jSwaggerApp } from '@kt/knife4j-vue3-ui'; import type { Knife4jService } from '@kt/knife4j-vue3-ui'; -const services: Knife4jService[] = [ +declare global { + interface Window { + __KNIFE4J_SERVICES__?: Knife4jService[]; + } +} + +const fallbackServices: Knife4jService[] = [ { name: '全量接口', url: '/api-json', @@ -26,37 +32,53 @@ const services: Knife4jService[] = [ }, ]; -const Knife4jPlayground = defineComponent({ - name: 'Knife4jPlayground', - setup: () => () => , -}); +const loadServices = async () => { + if (Array.isArray(window.__KNIFE4J_SERVICES__)) { + return window.__KNIFE4J_SERVICES__; + } -const router = createRouter({ - history: createWebHashHistory(), - routes: [ - { - path: '/', - redirect: '/home/0', - }, - { - component: Knife4jPlayground, - path: '/home/:service', - }, - { - component: Knife4jPlayground, - path: '/api/:service/:operationId(.*)', - }, - { - component: Knife4jPlayground, - path: '/static/:service/:staticType', - }, - { - component: Knife4jPlayground, - path: '/:pathMatch(.*)*', - }, - ], -}); + try { + const response = await fetch('services.json', { cache: 'no-store' }); + const data = (await response.json()) as Knife4jService[]; + return Array.isArray(data) && data.length ? data : fallbackServices; + } catch { + return fallbackServices; + } +}; -createApp(() => ) - .use(router) - .mount('#app'); +loadServices().then((services) => { + const Knife4jPlayground = defineComponent({ + name: 'Knife4jPlayground', + setup: () => () => , + }); + + const router = createRouter({ + history: createWebHashHistory(), + routes: [ + { + path: '/', + redirect: '/home/0', + }, + { + component: Knife4jPlayground, + path: '/home/:service', + }, + { + component: Knife4jPlayground, + path: '/api/:service/:operationId(.*)', + }, + { + component: Knife4jPlayground, + path: '/static/:service/:staticType', + }, + { + component: Knife4jPlayground, + path: '/:pathMatch(.*)*', + }, + ], + }); + + createApp(() => ) + .use(router) + .mount('#app'); +}); diff --git a/apps/playground/vite.config.ts b/apps/playground/vite.config.ts index 8707868..9bf8819 100644 --- a/apps/playground/vite.config.ts +++ b/apps/playground/vite.config.ts @@ -4,6 +4,7 @@ import vueJsx from '@vitejs/plugin-vue-jsx'; import { fileURLToPath, URL } from 'node:url'; export default defineConfig({ + base: './', plugins: [vue(), vueJsx()], resolve: { alias: { @@ -25,6 +26,10 @@ export default defineConfig({ target: 'http://127.0.0.1:48085', changeOrigin: true, }, + '/services.json': { + target: 'http://127.0.0.1:48085', + changeOrigin: true, + }, }, }, }); diff --git a/package.json b/package.json index 7db3edc..260d37e 100644 --- a/package.json +++ b/package.json @@ -5,13 +5,18 @@ "type": "module", "packageManager": "pnpm@10.33.0", "scripts": { - "build": "pnpm -r --sort run build", + "build": "pnpm --filter @kt/openapi-parser run build && pnpm --filter @kt/knife4j-vue3-ui run build && pnpm --filter @kt/knife4j-swagger-vue3-playground run build && pnpm --filter @kwitsukasa/knife4j-swagger-vue3 run build", "dev": "pnpm --filter @kt/knife4j-swagger-vue3-playground dev", - "typecheck": "pnpm -r --sort run typecheck" + "lint": "pnpm run typecheck", + "prepare": "husky", + "typecheck": "pnpm -r --sort run typecheck", + "verify:commit": "pnpm run lint && pnpm run build" }, "devDependencies": { + "@types/node": "^20.14.12", "@vitejs/plugin-vue": "^6.0.3", "@vitejs/plugin-vue-jsx": "^5.1.5", + "husky": "^9.1.7", "sass": "^1.97.3", "typescript": "^5.9.3", "vite": "^7.3.1", diff --git a/packages/knife4j-vue3-ui/src/components/Knife4jApiPanel.tsx b/packages/knife4j-vue3-ui/src/components/Knife4jApiPanel.tsx index 71a94ba..5913b4b 100644 --- a/packages/knife4j-vue3-ui/src/components/Knife4jApiPanel.tsx +++ b/packages/knife4j-vue3-ui/src/components/Knife4jApiPanel.tsx @@ -3,7 +3,6 @@ import { Button, Checkbox, Input, - Radio, RadioGroup, Select, SelectOption, @@ -30,7 +29,6 @@ import type { } from "@kt/openapi-parser"; import type { Knife4jDebugConfig, - Knife4jDebugParameter, Knife4jParameterIn, Knife4jService, } from "../types"; diff --git a/packages/nestjs-knife4j/package.json b/packages/nestjs-knife4j/package.json new file mode 100644 index 0000000..af270f9 --- /dev/null +++ b/packages/nestjs-knife4j/package.json @@ -0,0 +1,27 @@ +{ + "name": "@kwitsukasa/knife4j-swagger-vue3", + "version": "0.1.2", + "description": "KT Knife4j Swagger Vue3 UI integration for NestJS", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "license": "MIT", + "files": [ + "dist", + "ui" + ], + "scripts": { + "build": "tsc -p tsconfig.json && node scripts/copy-ui.mjs", + "prepack": "pnpm --dir ../.. run build", + "typecheck": "tsc -p tsconfig.json --noEmit" + }, + "peerDependencies": { + "@nestjs/common": "*" + }, + "devDependencies": { + "@nestjs/common": "^9.4.3" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + } +} diff --git a/packages/nestjs-knife4j/scripts/copy-ui.mjs b/packages/nestjs-knife4j/scripts/copy-ui.mjs new file mode 100644 index 0000000..4e1e57f --- /dev/null +++ b/packages/nestjs-knife4j/scripts/copy-ui.mjs @@ -0,0 +1,23 @@ +import { cpSync, existsSync, mkdirSync, rmSync, renameSync } from 'node:fs'; +import { dirname, resolve } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const currentDir = dirname(fileURLToPath(import.meta.url)); +const packageDir = resolve(currentDir, '..'); +const workspaceDir = resolve(packageDir, '../..'); +const playgroundDist = resolve(workspaceDir, 'apps/playground/dist'); +const packageUi = resolve(packageDir, 'ui'); +const indexHtml = resolve(packageUi, 'index.html'); +const docHtml = resolve(packageUi, 'doc.html'); + +if (!existsSync(playgroundDist)) { + throw new Error('Playground dist not found. Run playground build before packaging.'); +} + +rmSync(packageUi, { force: true, recursive: true }); +mkdirSync(packageUi, { recursive: true }); +cpSync(playgroundDist, packageUi, { recursive: true }); + +if (existsSync(indexHtml)) { + renameSync(indexHtml, docHtml); +} diff --git a/packages/nestjs-knife4j/src/index.ts b/packages/nestjs-knife4j/src/index.ts new file mode 100644 index 0000000..56ccab4 --- /dev/null +++ b/packages/nestjs-knife4j/src/index.ts @@ -0,0 +1,139 @@ +import type { INestApplication } from '@nestjs/common'; +import { createReadStream } from 'node:fs'; +import { stat } from 'node:fs/promises'; +import { extname, isAbsolute, relative, resolve } from 'node:path'; + +export interface Service { + name: string; + url: string; +} + +interface NormalizedPrefix { + prefix: string; + servicePath: string; +} + +interface ExpressStaticResponse { + end: () => void; + setHeader: (name: string, value: string) => void; + status?: (code: number) => { end: () => void }; +} + +const normalizePrefix = (prefix: string): NormalizedPrefix => { + const trimmedPrefix = prefix.trim() || '/'; + const withLeadingSlash = trimmedPrefix.startsWith('/') + ? trimmedPrefix + : `/${trimmedPrefix}`; + const normalizedPrefix = withLeadingSlash.endsWith('/') + ? withLeadingSlash.slice(0, -1) + : withLeadingSlash; + const prefixPath = normalizedPrefix || '/'; + const servicePath = + prefixPath === '/' ? '/services.json' : `${prefixPath}/services.json`; + + return { + prefix: prefixPath, + servicePath, + }; +}; + +const getContentType = (filePath: string) => { + const ext = extname(filePath).toLowerCase(); + const contentTypes: Record = { + '.css': 'text/css; charset=utf-8', + '.gif': 'image/gif', + '.html': 'text/html; charset=utf-8', + '.ico': 'image/x-icon', + '.js': 'application/javascript; charset=utf-8', + '.json': 'application/json; charset=utf-8', + '.map': 'application/json; charset=utf-8', + '.png': 'image/png', + '.svg': 'image/svg+xml', + '.webp': 'image/webp', + '.woff': 'font/woff', + '.woff2': 'font/woff2', + }; + + return contentTypes[ext] || 'application/octet-stream'; +}; + +const getStaticPathname = (url = '/') => { + const pathname = decodeURIComponent(url.split('?')[0] || '/'); + return pathname === '/' ? '/doc.html' : pathname; +}; + +const resolveStaticFile = async (uiRoot: string, url = '/') => { + const pathname = getStaticPathname(url); + const filePath = resolve(uiRoot, `.${pathname}`); + const relativePath = relative(uiRoot, filePath); + + if (relativePath.startsWith('..') || isAbsolute(relativePath)) { + return undefined; + } + + const fileStat = await stat(filePath).catch(() => undefined); + return fileStat?.isFile() ? filePath : undefined; +}; + +const sendExpressStaticFile = async ( + uiRoot: string, + request: { url?: string }, + response: ExpressStaticResponse, + next?: () => void, +) => { + const filePath = await resolveStaticFile(uiRoot, request.url); + + if (!filePath) { + next?.(); + if (!next) response.status?.(404).end(); + return; + } + + response.setHeader('Content-Type', getContentType(filePath)); + createReadStream(filePath).pipe(response as unknown as NodeJS.WritableStream); +}; + +export const knife4jSetup = async ( + app: INestApplication, + services: Service[], + prefix = '/', +) => { + const { prefix: staticPrefix, servicePath } = normalizePrefix(prefix); + const uiRoot = resolve(__dirname, '../ui'); + const adapter = app.getHttpAdapter(); + const adapterType = adapter.getType(); + const instance = adapter.getInstance(); + + if (!['express', 'fastify'].includes(adapterType)) { + throw new Error('Knife4j only supports express and fastify adapters.'); + } + + if (adapterType === 'express') { + app.use(servicePath, (_request: unknown, response: any) => { + response.send(services); + }); + app.use(staticPrefix, (request: any, response: any, next: () => void) => { + void sendExpressStaticFile(uiRoot, request, response, next); + }); + return; + } + + instance.get(servicePath, (_request: unknown, reply: any) => { + reply.send(services); + }); + instance.get(`${staticPrefix === '/' ? '' : staticPrefix}/*`, async ( + request: { params?: Record }, + reply: any, + ) => { + const wildcardPath = request.params?.['*'] || 'doc.html'; + const filePath = await resolveStaticFile(uiRoot, `/${wildcardPath}`); + + if (!filePath) { + reply.code(404).send(); + return; + } + + reply.header('Content-Type', getContentType(filePath)); + reply.send(createReadStream(filePath)); + }); +}; diff --git a/packages/nestjs-knife4j/tsconfig.json b/packages/nestjs-knife4j/tsconfig.json new file mode 100644 index 0000000..68417cb --- /dev/null +++ b/packages/nestjs-knife4j/tsconfig.json @@ -0,0 +1,14 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "isolatedModules": false, + "module": "Node16", + "moduleResolution": "Node16", + "outDir": "dist", + "rootDir": "src", + "types": ["node"] + }, + "include": ["src/**/*.ts"] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 72c6652..5cd5cfa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,12 +8,18 @@ importers: .: devDependencies: + '@types/node': + specifier: ^20.14.12 + version: 20.19.41 '@vitejs/plugin-vue': specifier: ^6.0.3 - version: 6.0.7(vite@7.3.5(sass@1.100.0))(vue@3.5.35(typescript@5.9.3)) + version: 6.0.7(vite@7.3.5(@types/node@20.19.41)(sass@1.100.0))(vue@3.5.35(typescript@5.9.3)) '@vitejs/plugin-vue-jsx': specifier: ^5.1.5 - version: 5.1.5(vite@7.3.5(sass@1.100.0))(vue@3.5.35(typescript@5.9.3)) + version: 5.1.5(vite@7.3.5(@types/node@20.19.41)(sass@1.100.0))(vue@3.5.35(typescript@5.9.3)) + husky: + specifier: ^9.1.7 + version: 9.1.7 sass: specifier: ^1.97.3 version: 1.100.0 @@ -22,7 +28,7 @@ importers: version: 5.9.3 vite: specifier: ^7.3.1 - version: 7.3.5(sass@1.100.0) + version: 7.3.5(@types/node@20.19.41)(sass@1.100.0) vue-tsc: specifier: ^3.2.4 version: 3.3.3(typescript@5.9.3) @@ -41,13 +47,13 @@ importers: devDependencies: '@vitejs/plugin-vue': specifier: ^6.0.3 - version: 6.0.7(vite@7.3.5(sass@1.100.0))(vue@3.5.35(typescript@5.9.3)) + version: 6.0.7(vite@7.3.5(@types/node@20.19.41)(sass@1.100.0))(vue@3.5.35(typescript@5.9.3)) '@vitejs/plugin-vue-jsx': specifier: ^5.1.5 - version: 5.1.5(vite@7.3.5(sass@1.100.0))(vue@3.5.35(typescript@5.9.3)) + version: 5.1.5(vite@7.3.5(@types/node@20.19.41)(sass@1.100.0))(vue@3.5.35(typescript@5.9.3)) vite: specifier: ^7.3.1 - version: 7.3.5(sass@1.100.0) + version: 7.3.5(@types/node@20.19.41)(sass@1.100.0) vue-tsc: specifier: ^3.2.4 version: 3.3.3(typescript@5.9.3) @@ -72,17 +78,23 @@ importers: devDependencies: '@vitejs/plugin-vue': specifier: ^6.0.3 - version: 6.0.7(vite@7.3.5(sass@1.100.0))(vue@3.5.35(typescript@5.9.3)) + version: 6.0.7(vite@7.3.5(@types/node@20.19.41)(sass@1.100.0))(vue@3.5.35(typescript@5.9.3)) '@vitejs/plugin-vue-jsx': specifier: ^5.1.5 - version: 5.1.5(vite@7.3.5(sass@1.100.0))(vue@3.5.35(typescript@5.9.3)) + version: 5.1.5(vite@7.3.5(@types/node@20.19.41)(sass@1.100.0))(vue@3.5.35(typescript@5.9.3)) vite: specifier: ^7.3.1 - version: 7.3.5(sass@1.100.0) + version: 7.3.5(@types/node@20.19.41)(sass@1.100.0) vue-tsc: specifier: ^3.2.4 version: 3.3.3(typescript@5.9.3) + packages/nestjs-knife4j: + devDependencies: + '@nestjs/common': + specifier: ^9.4.3 + version: 9.4.3(reflect-metadata@0.1.14)(rxjs@7.8.2) + packages/openapi-parser: {} packages: @@ -413,6 +425,26 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@lukeed/csprng@1.1.0': + resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==} + engines: {node: '>=8'} + + '@nestjs/common@9.4.3': + resolution: {integrity: sha512-Gd6D4IaYj01o14Bwv81ukidn4w3bPHCblMUq+SmUmWLyosK+XQmInCS09SbDDZyL8jy86PngtBLTdhJ2bXSUig==} + peerDependencies: + cache-manager: <=5 + class-transformer: '*' + class-validator: '*' + reflect-metadata: ^0.1.12 + rxjs: ^7.1.0 + peerDependenciesMeta: + cache-manager: + optional: true + class-transformer: + optional: true + class-validator: + optional: true + '@parcel/watcher-android-arm64@2.5.6': resolution: {integrity: sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==} engines: {node: '>= 10.0.0'} @@ -645,6 +677,9 @@ packages: '@types/estree@1.0.9': resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + '@types/node@20.19.41': + resolution: {integrity: sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ==} + '@types/web-bluetooth@0.0.21': resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==} @@ -1037,6 +1072,11 @@ packages: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} + husky@9.1.7: + resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==} + engines: {node: '>=18'} + hasBin: true + immutable@5.1.6: resolution: {integrity: sha512-q1swsS8K7L8usSHuOqF2TAoCCkonYz0SG38wLAggaa4Wml70zixIvt2ql4coQ2C2B3hTjltJry4r6bULwgAXLQ==} @@ -1048,6 +1088,10 @@ packages: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} + iterare@1.2.1: + resolution: {integrity: sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==} + engines: {node: '>=6'} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -1103,6 +1147,9 @@ packages: resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} engines: {node: '>= 20.19.0'} + reflect-metadata@0.1.14: + resolution: {integrity: sha512-ZhYeb6nRaXCfhnndflDK8qI6ZQ/YcWZCISRAWICW9XYqMUwjZM9Z0DveWX/ABN01oxSHwVxKQmxeYZSsm0jh5A==} + resize-observer-polyfill@1.5.1: resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==} @@ -1111,6 +1158,9 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + rxjs@7.8.2: + resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} + sass@1.100.0: resolution: {integrity: sha512-B5j0rYMlinhhOo9tjQebMVVn0TfyXAF+wB3b2ggZUuJ/is/Y+7+JGjirAMxHZ9Z3hIP98NPfamlAkBHa1lAaXQ==} engines: {node: '>=20.19.0'} @@ -1138,11 +1188,24 @@ packages: resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} engines: {node: '>=12.0.0'} + tslib@2.5.3: + resolution: {integrity: sha512-mSxlJJwl3BMEQCUNnxXBU9jP4JBktcEGhURcPR6VQVlnP0FdDEsIaz0C35dXNGLyRfrATNofF0F5p2KPxQgB+w==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} hasBin: true + uid@2.0.2: + resolution: {integrity: sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==} + engines: {node: '>=8'} + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + update-browserslist-db@1.2.3: resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} hasBin: true @@ -1518,6 +1581,16 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@lukeed/csprng@1.1.0': {} + + '@nestjs/common@9.4.3(reflect-metadata@0.1.14)(rxjs@7.8.2)': + dependencies: + iterare: 1.2.1 + reflect-metadata: 0.1.14 + rxjs: 7.8.2 + tslib: 2.5.3 + uid: 2.0.2 + '@parcel/watcher-android-arm64@2.5.6': optional: true @@ -1658,6 +1731,10 @@ snapshots: '@types/estree@1.0.9': {} + '@types/node@20.19.41': + dependencies: + undici-types: 6.21.0 + '@types/web-bluetooth@0.0.21': {} '@v-c/async-validator@1.0.1': {} @@ -1902,22 +1979,22 @@ snapshots: '@v-c/util': 1.0.19(vue@3.5.35(typescript@5.9.3)) vue: 3.5.35(typescript@5.9.3) - '@vitejs/plugin-vue-jsx@5.1.5(vite@7.3.5(sass@1.100.0))(vue@3.5.35(typescript@5.9.3))': + '@vitejs/plugin-vue-jsx@5.1.5(vite@7.3.5(@types/node@20.19.41)(sass@1.100.0))(vue@3.5.35(typescript@5.9.3))': dependencies: '@babel/core': 7.29.7 '@babel/plugin-syntax-typescript': 7.29.7(@babel/core@7.29.7) '@babel/plugin-transform-typescript': 7.29.7(@babel/core@7.29.7) '@rolldown/pluginutils': 1.0.1 '@vue/babel-plugin-jsx': 2.0.1(@babel/core@7.29.7) - vite: 7.3.5(sass@1.100.0) + vite: 7.3.5(@types/node@20.19.41)(sass@1.100.0) vue: 3.5.35(typescript@5.9.3) transitivePeerDependencies: - supports-color - '@vitejs/plugin-vue@6.0.7(vite@7.3.5(sass@1.100.0))(vue@3.5.35(typescript@5.9.3))': + '@vitejs/plugin-vue@6.0.7(vite@7.3.5(@types/node@20.19.41)(sass@1.100.0))(vue@3.5.35(typescript@5.9.3))': dependencies: '@rolldown/pluginutils': 1.0.1 - vite: 7.3.5(sass@1.100.0) + vite: 7.3.5(@types/node@20.19.41)(sass@1.100.0) vue: 3.5.35(typescript@5.9.3) '@volar/language-core@2.4.28': @@ -2176,6 +2253,8 @@ snapshots: gensync@1.0.0-beta.2: {} + husky@9.1.7: {} + immutable@5.1.6: {} is-extglob@2.1.1: @@ -2186,6 +2265,8 @@ snapshots: is-extglob: 2.1.1 optional: true + iterare@1.2.1: {} + js-tokens@4.0.0: {} jsesc@3.1.0: {} @@ -2225,6 +2306,8 @@ snapshots: readdirp@5.0.0: {} + reflect-metadata@0.1.14: {} + resize-observer-polyfill@1.5.1: {} rollup@4.61.0: @@ -2258,6 +2341,10 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.61.0 fsevents: 2.3.3 + rxjs@7.8.2: + dependencies: + tslib: 2.8.1 + sass@1.100.0: dependencies: chokidar: 5.0.0 @@ -2283,15 +2370,25 @@ snapshots: fdir: 6.5.0(picomatch@4.0.4) picomatch: 4.0.4 + tslib@2.5.3: {} + + tslib@2.8.1: {} + typescript@5.9.3: {} + uid@2.0.2: + dependencies: + '@lukeed/csprng': 1.1.0 + + undici-types@6.21.0: {} + update-browserslist-db@1.2.3(browserslist@4.28.2): dependencies: browserslist: 4.28.2 escalade: 3.2.0 picocolors: 1.1.1 - vite@7.3.5(sass@1.100.0): + vite@7.3.5(@types/node@20.19.41)(sass@1.100.0): dependencies: esbuild: 0.27.7 fdir: 6.5.0(picomatch@4.0.4) @@ -2300,6 +2397,7 @@ snapshots: rollup: 4.61.0 tinyglobby: 0.2.17 optionalDependencies: + '@types/node': 20.19.41 fsevents: 2.3.3 sass: 1.100.0 diff --git a/scripts/validate-commit-msg.mjs b/scripts/validate-commit-msg.mjs new file mode 100644 index 0000000..3b33c85 --- /dev/null +++ b/scripts/validate-commit-msg.mjs @@ -0,0 +1,24 @@ +import { readFileSync } from 'node:fs'; + +const messageFile = process.argv[2]; +const firstLine = readFileSync(messageFile, 'utf8').split(/\r?\n/)[0].trim(); + +const releaseMessages = /^(?:Merge|Revert|fixup!|squash!)/; +const ktCommitPattern = + /^(?:feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)(?:\([\w./-]+\))?: .+/; +const hasChineseText = /[\u4E00-\u9FFF]/; + +if (releaseMessages.test(firstLine)) { + process.exit(0); +} + +if (!ktCommitPattern.test(firstLine) || !hasChineseText.test(firstLine)) { + console.error( + [ + '提交信息格式不正确。', + '要求:英文类型前缀 + 可选 scope + 冒号空格 + 中文描述。', + '示例:feat(knife4j): 增加提交校验', + ].join('\n'), + ); + process.exit(1); +} diff --git a/tsconfig.base.json b/tsconfig.base.json index 2ee638f..2224f0b 100644 --- a/tsconfig.base.json +++ b/tsconfig.base.json @@ -11,6 +11,8 @@ "isolatedModules": true, "esModuleInterop": true, "skipLibCheck": true, - "forceConsistentCasingInFileNames": true + "forceConsistentCasingInFileNames": true, + "noUnusedLocals": true, + "noUnusedParameters": true } } diff --git a/tsconfig.json b/tsconfig.json index f9f8a6f..9497014 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,6 +8,9 @@ { "path": "./packages/knife4j-vue3-ui" }, + { + "path": "./packages/nestjs-knife4j" + }, { "path": "./apps/playground" }