feat: 发布自研 Knife4j 适配包

This commit is contained in:
sunlei 2026-06-03 16:07:41 +08:00
parent 628fd36796
commit 6b651fc612
15 changed files with 416 additions and 52 deletions

2
.gitignore vendored
View File

@ -6,3 +6,5 @@ dist
**/.turbo
*.log
*.tsbuildinfo
*.tgz
packages/nestjs-knife4j/ui

1
.husky/commit-msg Normal file
View File

@ -0,0 +1 @@
node scripts/validate-commit-msg.mjs "$1"

1
.husky/pre-commit Normal file
View File

@ -0,0 +1 @@
pnpm run verify:commit

View File

@ -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,12 +32,27 @@ const services: Knife4jService[] = [
},
];
const Knife4jPlayground = defineComponent({
const loadServices = async () => {
if (Array.isArray(window.__KNIFE4J_SERVICES__)) {
return window.__KNIFE4J_SERVICES__;
}
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;
}
};
loadServices().then((services) => {
const Knife4jPlayground = defineComponent({
name: 'Knife4jPlayground',
setup: () => () => <Knife4jSwaggerApp services={services} />,
});
});
const router = createRouter({
const router = createRouter({
history: createWebHashHistory(),
routes: [
{
@ -55,8 +76,9 @@ const router = createRouter({
path: '/:pathMatch(.*)*',
},
],
});
});
createApp(() => <RouterView />)
createApp(() => <RouterView />)
.use(router)
.mount('#app');
});

View File

@ -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,
},
},
},
});

View File

@ -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",

View File

@ -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";

View File

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

View File

@ -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);
}

View File

@ -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<string, string> = {
'.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<string, string> },
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));
});
};

View File

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

View File

@ -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

View File

@ -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);
}

View File

@ -11,6 +11,8 @@
"isolatedModules": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
"forceConsistentCasingInFileNames": true,
"noUnusedLocals": true,
"noUnusedParameters": true
}
}

View File

@ -8,6 +8,9 @@
{
"path": "./packages/knife4j-vue3-ui"
},
{
"path": "./packages/nestjs-knife4j"
},
{
"path": "./apps/playground"
}