refactor: 整体重构 (#1381)

* feat: pnpm new

* Refactor build and release workflows, update dependencies

Switch build scripts and workflows from npm to pnpm, update build and artifact paths, and simplify release workflow by removing version detection and changelog steps. Add new dependencies (silk-wasm, express, ws, node-pty-prebuilt-multiarch), update exports in package.json files, and add vite config for napcat-framework. Also, rename manifest.json for framework package and fix static asset copying in shell build config.
This commit is contained in:
手瓜一十雪
2025-11-13 15:39:42 +08:00
committed by GitHub
parent c3d1892545
commit ed19c52f25
778 changed files with 2356 additions and 26391 deletions

View File

@@ -0,0 +1,122 @@
import { PlayMode } from '@/const/enum';
import WebUIManager from '@/controllers/webui_manager';
import type {
FinalMusic,
Music163ListResponse,
Music163URLResponse,
} from '@/types/music';
/**
* 获取网易云音乐歌单
* @param id 歌单id
* @returns 歌单信息
*/
export const get163MusicList = async (id: string) => {
const res = await WebUIManager.proxy<Music163ListResponse>(
'https://wavesgame.top/playlist/track/all?id=' + id
);
// const res = await request.get<Music163ListResponse>(
// `https://wavesgame.top/playlist/track/all?id=${id}`
// )
if (res?.data?.code !== 200) {
throw new Error('获取歌曲列表失败');
}
return res.data;
};
/**
* 获取歌曲地址
* @param ids 歌曲id
* @returns 歌曲地址
*/
export const getSongsURL = async (ids: number[]) => {
const _ids = ids.reduce((prev, cur, index) => {
const groupIndex = Math.floor(index / 10);
if (!prev[groupIndex]) {
prev[groupIndex] = [];
}
prev[groupIndex].push(cur);
return prev;
}, [] as number[][]);
const res = await Promise.all(
_ids.map(async (id) => {
const res = await WebUIManager.proxy<Music163URLResponse>(
`https://wavesgame.top/song/url?id=${id.join(',')}`
);
if (res?.data?.code !== 200) {
throw new Error('获取歌曲地址失败');
}
return res.data.data;
})
);
const result = res.reduce((prev, cur) => {
return prev.concat(...cur);
}, []);
return result;
};
/**
* 获取网易云音乐歌单歌曲
* @param id 歌单id
* @returns 歌曲信息
*/
export const get163MusicListSongs = async (id: string) => {
const listRes = await get163MusicList(id);
const songs = listRes.songs.map((song) => song.id);
const songsRes = await getSongsURL(songs);
const finalMusic: FinalMusic[] = [];
for (let i = 0; i < listRes.songs.length; i++) {
const song = listRes.songs[i];
const music = songsRes.find((s) => s.id === song.id);
const songURL = music?.url;
if (songURL) {
finalMusic.push({
id: song.id,
url: songURL.replace(/http:\/\//, '//').replace(/https:\/\//, '//'),
title: song.name,
artist: song.ar.map((p) => p.name).join('/'),
cover: song.al.picUrl,
});
}
}
return finalMusic;
};
/**
* 获取随机音乐
* @param ids 歌曲id
* @param currentId 当前音乐id
* @returns 随机音乐id
*/
export const getRandomMusic = (ids: number[], currentId: number): number => {
const randomIndex = Math.floor(Math.random() * ids.length);
const randomId = ids[randomIndex];
if (randomId === currentId) {
return getRandomMusic(ids, currentId);
}
return randomId;
};
/**
* 获取下一首音乐id
* @param ids 歌曲id
* @param currentId 当前音乐ID
* @param mode 播放模式
*/
export const getNextMusic = (
musics: FinalMusic[],
currentId: number,
mode: PlayMode
): number => {
const ids = musics.map((music) => music.id);
if (mode === PlayMode.Loop) {
const currentIndex = ids.findIndex((id) => id === currentId);
const nextIndex = currentIndex + 1;
return ids[nextIndex] || ids[0];
}
if (mode === PlayMode.Random) {
return getRandomMusic(ids, currentId);
}
return currentId;
};

View File

@@ -0,0 +1,11 @@
/**
* 深拷贝
* @param obj 需要拷贝的对象
* @returns 拷贝后的对象
*/
export const deepClone = <T>(obj: T): T => {
// 我们这里只只用于配置项,所以直接使用字符串就行
const newObj = JSON.parse(JSON.stringify(obj));
return newObj;
};

View File

@@ -0,0 +1,248 @@
import type { Op } from 'quill';
import type { EmojiValue } from '@/components/chat_input/formats/emoji_blot';
import type { ImageValue } from '@/components/chat_input/formats/image_blot';
import type { ReplyBlockValue } from '@/components/chat_input/formats/reply_blot';
import {
type AllOB11WsResponse,
type OB11AllEvent,
type OB11GroupMessage,
type OB11Message,
type OB11Notice,
OB11NoticeType,
type OB11PrivateMessage,
type OB11Segment,
type OneBot11Lifecycle,
type RequestResponse,
} from '../types/onebot';
/**
* 获取事件名称
* @param event 事件类型
* @returns 事件名称
* @description 用于获取事件的名称
*/
export const getEventName = (event: OB11AllEvent['post_type']): string => {
switch (event) {
case 'message':
return '消息';
case 'notice':
return '通知';
case 'request':
return '请求';
case 'meta_event':
return '元事件';
case 'message_sent':
return '消息上报';
default:
return '未知';
}
};
/**
* 获取生命周期事件名称
* @param event 生命周期事件类型
* @returns 生命周期事件名称
* @description 用于获取生命周期事件的名称
*/
export const getLifecycleName = (
event: OneBot11Lifecycle['sub_type']
): string => {
switch (event) {
case 'enable':
return '启用';
case 'disable':
return '停用';
case 'connect':
return '连接';
default:
return '未知';
}
};
/**
* 获取生命周期事件Chip颜色
* @param event 生命周期事件类型
* @returns 生命周期事件颜色
* @description 用于获取生命周期事件的颜色
*/
export const getLifecycleColor = (
event: OneBot11Lifecycle['sub_type']
): 'success' | 'danger' | 'default' => {
switch (event) {
case 'enable':
return 'success';
case 'disable':
return 'danger';
case 'connect':
return 'success';
default:
return 'default';
}
};
/**
* 判断 OneBot WS 返回值是否为事件
* @param data OneBot WS 返回值
* @returns 是否为事件
* @description 用于判断 OneBot WS 返回值是否为事件
*/
export const isOB11Event = (data: AllOB11WsResponse): data is OB11AllEvent => {
return 'post_type' in data;
};
/**
* 判断 OneBot WS 返回值是否为请求响应
* @param data OneBot WS 返回值
* @returns 是否为请求响应
* @description 用于判断 OneBot WS 返回值是否为请求响应
*/
export const isOB11RequestResponse = (
data: AllOB11WsResponse
): data is RequestResponse => {
return 'status' in data && 'retcode' in data;
};
/**
* 获取请求响应状态文本
* @param status 请求响应状态
* @returns 请求响应状态文本
* @description 用于获取请求响应状态的文本
*/
export const getResponseStatusText = (
status: RequestResponse['status']
): string => {
switch (status) {
case 'ok':
return '成功';
case 'failed':
return '失败';
case 'async':
return '异步';
default:
return '未知';
}
};
/**
* 获取请求响应状态颜色
* @param status 请求响应状态
* @returns 请求响应状态颜色
* @description 用于获取请求响应状态的颜色
*/
export const getResponseStatusColor = (
status: RequestResponse['status']
): 'success' | 'danger' | 'warning' | 'default' => {
switch (status) {
case 'ok':
return 'success';
case 'failed':
return 'danger';
case 'async':
return 'warning';
default:
return 'default';
}
};
/**
* 获取通知类型名称
* @param type 通知类型
* @returns 通知类型名称
* @description 用于获取通知类型的名称
*/
export const getNoticeTypeName = (type: OB11Notice['notice_type']): string => {
switch (type) {
case OB11NoticeType.GroupUpload:
return '群文件上传';
case OB11NoticeType.GroupAdmin:
return '群管理员变动';
case OB11NoticeType.GroupDecrease:
return '群成员减少';
case OB11NoticeType.GroupIncrease:
return '群成员增加';
case OB11NoticeType.GroupBan:
return '群禁言';
case OB11NoticeType.FriendAdd:
return '好友添加';
case OB11NoticeType.GroupRecall:
return '群消息撤回';
case OB11NoticeType.FriendRecall:
return '好友消息撤回';
case OB11NoticeType.Notify:
return '通知';
case OB11NoticeType.GroupMsgEmojiLike:
return '群消息表情回应';
case OB11NoticeType.GroupEssence:
return '群消息精华';
case OB11NoticeType.GroupCard:
return '群名片更新';
default:
return '未知';
}
};
/**
* 判断 OneBot 消息是否为私聊消息
* @param data OneBot 消息
* @returns 是否为私聊消息
* @description 用于判断 OneBot 消息是否为私聊消息
*/
export const isOB11PrivateMessage = (
data: OB11Message
): data is OB11PrivateMessage => {
return data.message_type === 'private';
};
/**
* 判断 OneBot 消息是否为群消息
* @param data OneBot 消息
* @returns 是否为群消息
* @description 用于判断 OneBot 消息是否为群消息
*/
export const isOB11GroupMessage = (
data: OB11Message
): data is OB11GroupMessage => {
return data.message_type === 'group';
};
/**
* 将 Quill Delta 转换为 OneBot 消息
* @param op Quill Delta
* @returns OneBot 消息
* @description 用于将 Quill Delta 转换为 OneBot 消息
*/
export const quillToMessage = (op: Op) => {
let message: OB11Segment = {
type: 'text',
data: {
text: op.insert as string,
},
};
if (typeof op.insert !== 'string') {
if (op.insert?.image) {
message = {
type: 'image',
data: {
file: (op.insert.image as ImageValue).src,
},
};
} else if (op.insert?.emoji) {
message = {
type: 'face',
data: {
id: (op.insert.emoji as EmojiValue).id,
},
};
} else if (op.insert?.reply) {
message = {
type: 'reply',
data: {
id: (op.insert.reply as ReplyBlockValue).messageId,
},
};
}
}
return message;
};

View File

@@ -0,0 +1,13 @@
import type { QQItem } from '@/components/quick_login';
/**
* 判断 QQ 快速登录列表项是否为 QQ 信息
* @param data QQ 快速登录列表项
* @returns 是否为 QQ 信息
* @description 用于判定 QQ 快速登录列表项是否为 QQ 信息
*/
export const isQQQuickNewItem = (
data?: QQItem | LoginListItem | null
): data is LoginListItem => {
return !!data && 'nickName' in data && 'faceUrl' in data;
};

View File

@@ -0,0 +1,64 @@
import axios from 'axios';
import key from '@/const/key';
export const serverRequest = axios.create({
timeout: 5000,
});
export const request = axios.create({
timeout: 10000,
});
export const requestServerWithFetch = async (
url: string,
options: RequestInit
) => {
const token = localStorage.getItem(key.token);
if (token) {
options.headers = {
...options.headers,
Authorization: `Bearer ${JSON.parse(token)}`,
};
}
const baseURL = '/api';
const response = await fetch(baseURL + url, options);
return response;
};
serverRequest.interceptors.request.use((config) => {
const baseURL = '/api';
config.baseURL = baseURL;
const token = localStorage.getItem(key.token);
if (token) {
config.headers['Authorization'] = `Bearer ${JSON.parse(token)}`;
}
return config;
});
serverRequest.interceptors.response.use((response) => {
// 如果是流式传输的文件
if (response.headers['content-type'] === 'application/octet-stream') {
return response;
}
if (response.data.code !== 0) {
if (response.data.message === 'Unauthorized') {
const token = localStorage.getItem(key.token);
if (token && JSON.parse(token)) {
localStorage.removeItem(key.token);
window.location.reload();
}
}
throw new Error(response.data.message);
}
return response;
});

View File

@@ -0,0 +1,97 @@
import { LogLevel } from '@/const/enum';
export const gradientText = (
text: string,
startColor: [number, number, number] = [255, 0, 0],
endColor: [number, number, number] = [0, 0, 255],
bold: boolean = false,
italic: boolean = false,
underline: boolean = false
) => {
const steps = text.length;
const colorStep = startColor.map(
(start, index) => (endColor[index] - start) / steps
);
let coloredText = '';
for (let i = 0; i < steps; i++) {
const color = startColor.map((start, index) =>
Math.round(start + colorStep[index] * i)
);
coloredText += `\x1b[38;2;${color[0]};${color[1]};${color[2]}m${text[i]}`;
}
// 添加样式
if (bold) {
coloredText = `\x1b[1m${coloredText}`;
}
if (italic) {
coloredText = `\x1b[3m${coloredText}`;
}
if (underline) {
coloredText = `\x1b[4m${coloredText}`;
}
return coloredText + '\x1b[0m'; // 重置颜色和样式
};
export const logColor = {
[LogLevel.DEBUG]: 'green',
[LogLevel.INFO]: 'black',
[LogLevel.WARN]: 'yellow',
[LogLevel.ERROR]: 'red',
[LogLevel.FATAL]: 'red',
} as const;
export const colorizeLogLevel = (content: string) => {
const logLevel = content.match(/\[[a-zA-Z]+\]/) || [];
let _content = content;
const level =
(logLevel?.[0]?.replace('[', '').replace(']', '') as LogLevel) ??
LogLevel.INFO;
const color = logColor[level];
switch (color) {
case 'green':
_content = `\x1b[32m${_content}\x1b[0m`;
break;
case 'black':
_content = `\x1b[30m${_content}\x1b[0m`;
break;
case 'yellow':
_content = `\x1b[33m${_content}\x1b[0m`;
break;
case 'red':
_content = `\x1b[31m${_content}\x1b[0m`;
break;
default:
_content = `\x1b[30m${_content}\x1b[0m`;
}
return {
content: _content,
level,
};
};
export const colorizeLogLevelWithTag = (content: string, level: LogLevel) => {
let _content = content;
switch (level) {
case LogLevel.DEBUG:
_content = `\x1b[32m[DEBUG] ${content}\x1b[0m`;
break;
case LogLevel.INFO:
_content = `\x1b[30m[INFO] ${content}\x1b[0m`;
break;
case LogLevel.WARN:
_content = `\x1b[33m[WARN] ${content}\x1b[0m`;
break;
case LogLevel.ERROR:
_content = `\x1b[31m[ERROR] ${content}\x1b[0m`;
break;
case LogLevel.FATAL:
_content = `\x1b[31m[FATAL] ${content}\x1b[0m`;
break;
default:
_content = `\x1b[30m${content}\x1b[0m`;
}
return _content;
};

View File

@@ -0,0 +1,141 @@
import { request } from './request';
const style = document.createElement('style');
document.head.appendChild(style);
export function loadTheme () {
request('/files/theme.css?_t=' + Date.now())
.then((res) => res.data)
.then((css) => {
style.innerHTML = css;
})
.catch(() => {
console.error('Failed to load theme.css');
});
}
export const colorKeys = [
'--heroui-background',
'--heroui-foreground-50',
'--heroui-foreground-100',
'--heroui-foreground-200',
'--heroui-foreground-300',
'--heroui-foreground-400',
'--heroui-foreground-500',
'--heroui-foreground-600',
'--heroui-foreground-700',
'--heroui-foreground-800',
'--heroui-foreground-900',
'--heroui-foreground',
'--heroui-content1',
'--heroui-content1-foreground',
'--heroui-content2',
'--heroui-content2-foreground',
'--heroui-content3',
'--heroui-content3-foreground',
'--heroui-content4',
'--heroui-content4-foreground',
'--heroui-default-50',
'--heroui-default-100',
'--heroui-default-200',
'--heroui-default-300',
'--heroui-default-400',
'--heroui-default-500',
'--heroui-default-600',
'--heroui-default-700',
'--heroui-default-800',
'--heroui-default-900',
'--heroui-default-foreground',
'--heroui-default',
'--heroui-danger-50',
'--heroui-danger-100',
'--heroui-danger-200',
'--heroui-danger-300',
'--heroui-danger-400',
'--heroui-danger-500',
'--heroui-danger-600',
'--heroui-danger-700',
'--heroui-danger-800',
'--heroui-danger-900',
'--heroui-danger-foreground',
'--heroui-danger',
'--heroui-primary-50',
'--heroui-primary-100',
'--heroui-primary-200',
'--heroui-primary-300',
'--heroui-primary-400',
'--heroui-primary-500',
'--heroui-primary-600',
'--heroui-primary-700',
'--heroui-primary-800',
'--heroui-primary-900',
'--heroui-primary-foreground',
'--heroui-primary',
'--heroui-secondary-50',
'--heroui-secondary-100',
'--heroui-secondary-200',
'--heroui-secondary-300',
'--heroui-secondary-400',
'--heroui-secondary-500',
'--heroui-secondary-600',
'--heroui-secondary-700',
'--heroui-secondary-800',
'--heroui-secondary-900',
'--heroui-secondary-foreground',
'--heroui-secondary',
'--heroui-success-50',
'--heroui-success-100',
'--heroui-success-200',
'--heroui-success-300',
'--heroui-success-400',
'--heroui-success-500',
'--heroui-success-600',
'--heroui-success-700',
'--heroui-success-800',
'--heroui-success-900',
'--heroui-success-foreground',
'--heroui-success',
'--heroui-warning-50',
'--heroui-warning-100',
'--heroui-warning-200',
'--heroui-warning-300',
'--heroui-warning-400',
'--heroui-warning-500',
'--heroui-warning-600',
'--heroui-warning-700',
'--heroui-warning-800',
'--heroui-warning-900',
'--heroui-warning-foreground',
'--heroui-warning',
'--heroui-focus',
'--heroui-overlay',
'--heroui-divider',
'--heroui-code-background',
'--heroui-strong',
'--heroui-code-mdx',
] as const;
export const generateTheme = (theme: ThemeConfig, validField?: string) => {
let css = `:root ${validField ? `.${validField}` : ''}, .light ${validField ? `.${validField}` : ''}, [data-theme="light"] ${validField ? `.${validField}` : ''} {`;
for (const key in theme.light) {
const _key = key as keyof ThemeConfigItem;
css += `${_key}: ${theme.light[_key]};`;
}
css += '}';
css += `.dark ${validField ? `.${validField}` : ''}, [data-theme="dark"] ${validField ? `.${validField}` : ''} {`;
for (const key in theme.dark) {
const _key = key as keyof ThemeConfigItem;
css += `${_key}: ${theme.dark[_key]};`;
}
css += '}';
return css;
};

View File

@@ -0,0 +1,52 @@
/**
* 休眠函数
* @param ms 休眠时间
* @returns Promise<void>
*/
export const sleep = (ms: number) =>
new Promise((resolve) => setTimeout(resolve, ms));
/**
* 获取距离当前时间的发布时间
* @param time 发布时间
* @returns string
* @example getReleaseTime("2021-10-10T10:10:10Z") => "1天前"
*/
export const getReleaseTime = (time: string) => {
const releaseTime = new Date(time).getTime();
const nowTime = new Date().getTime();
const diffTime = nowTime - releaseTime;
const diffDays = Math.floor(diffTime / (24 * 3600 * 1000));
const diffHours = Math.floor((diffTime % (24 * 3600 * 1000)) / (3600 * 1000));
const diffMinutes = Math.floor((diffTime % (3600 * 1000)) / (60 * 1000));
const diffSeconds = Math.floor((diffTime % (60 * 1000)) / 1000);
if (diffDays > 0) {
return `${diffDays}天前`;
} else if (diffHours > 0) {
return `${diffHours}小时前`;
} else if (diffMinutes > 0) {
return `${diffMinutes}分钟前`;
} else {
return `${diffSeconds}秒前`;
}
};
const formatNumber = (n: number) => n.toString().padStart(2, '0');
/**
* 将时间戳转换为日期字符串
* @param timestamp 时间戳
* @returns string
* @example timestampToDateString(163383301) => "2021-10-10 10:10:10"
*/
export const timestampToDateString = (timestamp: number) => {
timestamp = timestamp * 1000;
const date = new Date(timestamp);
const year = date.getFullYear();
const month = date.getMonth() + 1;
const day = date.getDate();
const hour = date.getHours();
const minute = date.getMinutes();
const second = date.getSeconds();
return `${year}-${formatNumber(month)}-${formatNumber(day)} ${formatNumber(hour)}:${formatNumber(minute)}:${formatNumber(second)}`;
};

View File

@@ -0,0 +1,45 @@
import { AxiosResponse } from 'axios';
/**
* 打开链接
* @param url 链接地址
* @param newTab 是否在新标签页打开
*/
export function openUrl (url: string, newTab = true) {
if (newTab) {
window.open(url);
} else {
window.location.href = url;
}
}
/**
* 将Axios的响应转换为标准的HTTP报文
* @param response Axios响应
* @returns 标准的HTTP报文
*/
export function parseAxiosResponse (
response: AxiosResponse,
http_version: string = 'HTTP/1.1'
) {
if (!response?.status) {
return 'No response';
}
const statusLine = `${http_version} ${response.status} ${response.statusText}`;
const headers = Object.entries(response.headers)
.map(([key, value]) => `${key}: ${value}`)
.join('\r\n');
const body = response.data
? `\r\n\r\n${typeof response.data === 'string' ? JSON.stringify(JSON.parse(response.data), null, 2) : JSON.stringify(response.data, null, 2)}`
: '';
return `${statusLine}\r\n${headers}${body}`;
}
/**
* 判断是否为URI
* @param uri URI
* @returns 是否为URI
*/
export const isURI = (uri: string) => {
return /^(http|https|file):\/\/.*/.test(uri);
};

View File

@@ -0,0 +1,36 @@
/**
* 版本号转为数字
* @param version 版本号
* @returns 版本号数字
*/
export const versionToNumber = (version: string): number => {
const finalVersionString = version.replace(/^v/, '');
const versionArray = finalVersionString.split('.');
const versionNumber =
parseInt(versionArray[2]) +
parseInt(versionArray[1]) * 100 +
parseInt(versionArray[0]) * 10000;
return versionNumber;
};
/**
* 比较版本号
* @param version1 版本号1
* @param version2 版本号2
* @returns 比较结果
* 0: 相等
* 1: version1 > version2
* -1: version1 < version2
*/
export const compareVersion = (version1: string, version2: string): number => {
const versionNumber1 = versionToNumber(version1);
const versionNumber2 = versionToNumber(version2);
if (versionNumber1 === versionNumber2) {
return 0;
}
return versionNumber1 > versionNumber2 ? 1 : -1;
};

View File

@@ -0,0 +1,263 @@
import {
ZodArray,
ZodBigInt,
ZodBoolean,
ZodDate,
ZodDefault,
ZodEffects,
ZodEnum,
ZodLazy,
ZodLiteral,
ZodNull,
ZodNullable,
ZodNumber,
ZodObject,
ZodOptional,
ZodRecord,
ZodSchema,
ZodString,
ZodTuple,
ZodTypeAny,
ZodUnion,
ZodUnknown,
} from 'zod';
export type LiteralValue = string | number | boolean | null;
export type ParsedSchema = {
name?: string
type: string | string[]
optional: boolean
value?: LiteralValue
enum?: LiteralValue[]
children?: ParsedSchema[]
description?: string
};
export function parse (
schema: ZodTypeAny,
name?: string,
isRoot = true
): ParsedSchema | ParsedSchema[] {
const optional = schema.isOptional ? schema.isOptional() : false;
const description = schema.description;
if (schema instanceof ZodString) {
return { name, type: 'string', optional, description };
}
if (schema instanceof ZodNumber) {
return { name, type: 'number', optional, description };
}
if (schema instanceof ZodBoolean) {
return { name, type: 'boolean', optional, description };
}
if (schema instanceof ZodBigInt) {
return { name, type: 'bigint', optional, description };
}
if (schema instanceof ZodDate) {
return { name, type: 'date', optional, description };
}
if (schema instanceof ZodUnknown) {
return { name, type: 'unknown', optional, description };
}
if (schema instanceof ZodLiteral) {
return {
name,
type: 'value',
optional,
value: schema._def.value as LiteralValue,
description,
};
}
if (schema instanceof ZodEnum) {
const data = {
name,
type: 'enum',
optional,
enum: schema._def.values as LiteralValue[],
description,
};
return data;
}
if (schema instanceof ZodUnion) {
const options = schema._def.options;
const parsedOptions = options.map(
(option: ZodTypeAny) => parse(option, undefined, false) as ParsedSchema
);
const basicTypes = [
'string',
'number',
'boolean',
'bigint',
'date',
'unknown',
'value',
'enum',
];
const optionTypes: (string | string[])[] = parsedOptions.map(
(option: ParsedSchema) => option.type
);
const isAllBasicTypes = optionTypes.every((type) =>
basicTypes.includes(Array.isArray(type) ? type[0] : type)
);
if (isAllBasicTypes) {
const types = [
...new Set(
optionTypes.flatMap((type) => (Array.isArray(type) ? type[0] : type))
),
];
return { name, type: types, optional, description };
} else {
return {
name,
type: 'union',
optional,
children: parsedOptions,
description,
};
}
}
if (schema instanceof ZodObject) {
const shape = schema._def.shape();
const children = Object.keys(shape).map((key) =>
parse(shape[key], key, false)
) as ParsedSchema[];
if (isRoot) {
return children;
} else {
return { name, type: 'object', optional, children, description };
}
}
if (schema instanceof ZodArray) {
const childSchema = parse(
schema._def.type,
undefined,
false
) as ParsedSchema;
return {
name,
type: 'array',
optional,
children: Array.isArray(childSchema) ? childSchema : [childSchema],
description,
};
}
if (schema instanceof ZodNullable || schema instanceof ZodDefault) {
return parse(schema._def.innerType, name);
}
if (schema instanceof ZodOptional) {
const data = parse(schema._def.innerType, name);
if (Array.isArray(data)) {
data.forEach((item) => {
item.optional = true;
item.description = description;
});
} else {
data.optional = true;
data.description = description;
}
return data;
}
if (schema instanceof ZodRecord) {
const valueType = parse(schema._def.valueType) as ParsedSchema;
return {
name,
type: 'record',
optional,
children: [valueType],
description,
};
}
if (schema instanceof ZodTuple) {
const items: ParsedSchema[] = schema._def.items.map((item: ZodTypeAny) =>
parse(item)
);
return { name, type: 'tuple', optional, children: items, description };
}
if (schema instanceof ZodNull) {
return { name, type: 'null', optional, description };
}
if (schema instanceof ZodLazy) {
return parse(schema._def.getter(), name);
}
if (schema instanceof ZodEffects) {
return parse(schema._def.schema, name);
}
return { name, type: 'unknown', optional, description };
}
const generateDefault = (schema: ZodSchema): unknown => {
if (schema instanceof ZodObject) {
const obj: Record<string, unknown> = {};
for (const key in schema.shape) {
obj[key] = generateDefault(schema.shape[key]);
}
return obj;
}
if (schema instanceof ZodString) {
return 'textValue';
}
if (schema instanceof ZodNumber) {
return 0;
}
if (schema instanceof ZodBoolean) {
return false;
}
if (schema instanceof ZodArray) {
return [];
}
if (schema instanceof ZodUnion) {
return generateDefault(schema._def.options[0]);
}
if (schema instanceof ZodEnum) {
return schema._def.values[0];
}
if (schema instanceof ZodLiteral) {
return schema._def.value;
}
if (schema instanceof ZodNullable) {
return null;
}
if (schema instanceof ZodOptional) {
return generateDefault(schema._def.innerType);
}
if (schema instanceof ZodRecord) {
return {};
}
if (schema instanceof ZodTuple) {
return schema._def.items.map((item: ZodTypeAny) => generateDefault(item));
}
if (schema instanceof ZodNull) {
return null;
}
if (schema instanceof ZodLazy) {
return generateDefault(schema._def.getter());
}
if (schema instanceof ZodEffects) {
return generateDefault(schema._def.schema);
}
return null;
};
export const generateDefaultJson = (schema: ZodSchema) => {
return JSON.stringify(generateDefault(schema), null, 2);
};