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"
}