forked from wangziqi/gongxue-base
Compare commits
3 Commits
codex/wzq
...
187e4f2af0
| Author | SHA1 | Date | |
|---|---|---|---|
| 187e4f2af0 | |||
| 619b3b22bd | |||
| b5ef91bf2d |
@@ -31,6 +31,11 @@ import { message } from '../../ui/app-message';
|
|||||||
|
|
||||||
const { RangePicker } = DatePicker;
|
const { RangePicker } = DatePicker;
|
||||||
|
|
||||||
|
const isFormValidationError = (error: unknown) =>
|
||||||
|
typeof error === 'object'
|
||||||
|
&& error !== null
|
||||||
|
&& Array.isArray((error as { errorFields?: unknown }).errorFields);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const ExpensesPage: React.FC = () => {
|
const ExpensesPage: React.FC = () => {
|
||||||
@@ -157,16 +162,16 @@ const ExpensesPage: React.FC = () => {
|
|||||||
|
|
||||||
const handleRoomExpense = async () => {
|
const handleRoomExpense = async () => {
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
const values = await roomForm.validateFields();
|
|
||||||
const payload = {
|
|
||||||
roomId: values.roomId,
|
|
||||||
expenseType: values.expenseType,
|
|
||||||
amount: values.amount,
|
|
||||||
periodStart: values.period[0].format('YYYY-MM-DD'),
|
|
||||||
periodEnd: values.period[1].format('YYYY-MM-DD'),
|
|
||||||
description: values.description,
|
|
||||||
};
|
|
||||||
try {
|
try {
|
||||||
|
const values = await roomForm.validateFields();
|
||||||
|
const payload = {
|
||||||
|
roomId: values.roomId,
|
||||||
|
expenseType: values.expenseType,
|
||||||
|
amount: values.amount,
|
||||||
|
periodStart: values.period[0].format('YYYY-MM-DD'),
|
||||||
|
periodEnd: values.period[1].format('YYYY-MM-DD'),
|
||||||
|
description: values.description,
|
||||||
|
};
|
||||||
if (editingRoom) {
|
if (editingRoom) {
|
||||||
await api.put(`/expenses/room/${editingRoom.id}`, payload);
|
await api.put(`/expenses/room/${editingRoom.id}`, payload);
|
||||||
message.success('更新成功');
|
message.success('更新成功');
|
||||||
@@ -179,7 +184,9 @@ const ExpensesPage: React.FC = () => {
|
|||||||
roomForm.resetFields();
|
roomForm.resetFields();
|
||||||
fetchData();
|
fetchData();
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
message.error(e?.message || '操作失败');
|
if (!isFormValidationError(e)) {
|
||||||
|
message.error(e?.message || '操作失败');
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
}
|
}
|
||||||
@@ -187,16 +194,16 @@ const ExpensesPage: React.FC = () => {
|
|||||||
|
|
||||||
const handlePersonalExpense = async () => {
|
const handlePersonalExpense = async () => {
|
||||||
setSaving(true);
|
setSaving(true);
|
||||||
const values = await personalForm.validateFields();
|
|
||||||
const payload = {
|
|
||||||
studentId: values.studentId,
|
|
||||||
roomId: values.roomId,
|
|
||||||
expenseType: values.expenseType,
|
|
||||||
amount: values.amount,
|
|
||||||
expenseDate: values.expenseDate.format('YYYY-MM-DD'),
|
|
||||||
description: values.description,
|
|
||||||
};
|
|
||||||
try {
|
try {
|
||||||
|
const values = await personalForm.validateFields();
|
||||||
|
const payload = {
|
||||||
|
studentId: values.studentId,
|
||||||
|
roomId: values.roomId,
|
||||||
|
expenseType: values.expenseType,
|
||||||
|
amount: values.amount,
|
||||||
|
expenseDate: values.expenseDate.format('YYYY-MM-DD'),
|
||||||
|
description: values.description,
|
||||||
|
};
|
||||||
if (editingPersonal) {
|
if (editingPersonal) {
|
||||||
await api.put(`/expenses/personal/${editingPersonal.id}`, payload);
|
await api.put(`/expenses/personal/${editingPersonal.id}`, payload);
|
||||||
message.success('更新成功');
|
message.success('更新成功');
|
||||||
@@ -209,7 +216,9 @@ const ExpensesPage: React.FC = () => {
|
|||||||
personalForm.resetFields();
|
personalForm.resetFields();
|
||||||
fetchData();
|
fetchData();
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
message.error(e?.message || '操作失败');
|
if (!isFormValidationError(e)) {
|
||||||
|
message.error(e?.message || '操作失败');
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
"license": "UNLICENSED",
|
"license": "UNLICENSED",
|
||||||
"author": "",
|
"author": "",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "SEED_DEV=true nest start --watch -p tsconfig.build.json",
|
"dev": "cross-env SEED_DEV=true nest start --watch -p tsconfig.build.json",
|
||||||
"build": "nest build -p tsconfig.build.json",
|
"build": "nest build -p tsconfig.build.json",
|
||||||
"start:dev": "nest start --watch",
|
"start:dev": "nest start --watch",
|
||||||
"format": "oxfmt",
|
"format": "oxfmt",
|
||||||
@@ -67,6 +67,7 @@
|
|||||||
"@types/passport-jwt": "^4.0.1",
|
"@types/passport-jwt": "^4.0.1",
|
||||||
"@types/passport-local": "^1.0.38",
|
"@types/passport-local": "^1.0.38",
|
||||||
"@types/supertest": "^7.0.0",
|
"@types/supertest": "^7.0.0",
|
||||||
|
"cross-env": "^10.1.0",
|
||||||
"eslint": "^9.18.0",
|
"eslint": "^9.18.0",
|
||||||
"globals": "^17.0.0",
|
"globals": "^17.0.0",
|
||||||
"jest": "^30.0.0",
|
"jest": "^30.0.0",
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { InjectRepository } from '@nestjs/typeorm';
|
|||||||
import { Repository } from 'typeorm';
|
import { Repository } from 'typeorm';
|
||||||
import { Student } from '../entities/student.entity';
|
import { Student } from '../entities/student.entity';
|
||||||
import { StudentDingMapping } from '../entities/student-ding-mapping.entity';
|
import { StudentDingMapping } from '../entities/student-ding-mapping.entity';
|
||||||
|
import { IntegrationConfigService } from './config/integration-config.service';
|
||||||
|
|
||||||
// ── Types ──
|
// ── Types ──
|
||||||
|
|
||||||
@@ -18,6 +19,11 @@ interface DingTalkTokenResponse {
|
|||||||
expireIn: number;
|
expireIn: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface DingTalkCredentials {
|
||||||
|
appKey: string;
|
||||||
|
appSecret: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface DingTalkUserListResponse {
|
interface DingTalkUserListResponse {
|
||||||
errcode: number;
|
errcode: number;
|
||||||
errmsg: string;
|
errmsg: string;
|
||||||
@@ -168,6 +174,7 @@ export interface DingTalkScheduleResult {
|
|||||||
export class DingTalkService {
|
export class DingTalkService {
|
||||||
private readonly logger = new Logger(DingTalkService.name);
|
private readonly logger = new Logger(DingTalkService.name);
|
||||||
private accessToken: string | null = null;
|
private accessToken: string | null = null;
|
||||||
|
private accessTokenCredentialKey: string | null = null;
|
||||||
private tokenExpiresAt = 0;
|
private tokenExpiresAt = 0;
|
||||||
private apiRequestCount = 0;
|
private apiRequestCount = 0;
|
||||||
|
|
||||||
@@ -180,10 +187,28 @@ export class DingTalkService {
|
|||||||
private readonly studentRepo: Repository<Student>,
|
private readonly studentRepo: Repository<Student>,
|
||||||
@InjectRepository(StudentDingMapping)
|
@InjectRepository(StudentDingMapping)
|
||||||
private readonly studentDingMappingRepo: Repository<StudentDingMapping>,
|
private readonly studentDingMappingRepo: Repository<StudentDingMapping>,
|
||||||
|
private readonly integrationConfigService?: IntegrationConfigService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
private get configured(): boolean {
|
private async getCredentials(): Promise<DingTalkCredentials | null> {
|
||||||
return !!(process.env.DINGTALK_APP_KEY && process.env.DINGTALK_APP_SECRET);
|
const rawConfig = await this.integrationConfigService?.getRawConfig('DINGTALK');
|
||||||
|
const dbAppKey = typeof rawConfig?.agentId === 'string' ? rawConfig.agentId.trim() : '';
|
||||||
|
const dbAppSecret = typeof rawConfig?.appSecret === 'string' ? rawConfig.appSecret.trim() : '';
|
||||||
|
if (dbAppKey && dbAppSecret) {
|
||||||
|
return { appKey: dbAppKey, appSecret: dbAppSecret };
|
||||||
|
}
|
||||||
|
|
||||||
|
const envAppKey = process.env.DINGTALK_APP_KEY?.trim();
|
||||||
|
const envAppSecret = process.env.DINGTALK_APP_SECRET?.trim();
|
||||||
|
if (envAppKey && envAppSecret) {
|
||||||
|
return { appKey: envAppKey, appSecret: envAppSecret };
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async isConfigured(): Promise<boolean> {
|
||||||
|
return !!(await this.getCredentials());
|
||||||
}
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════
|
// ═══════════════════════════════════════════
|
||||||
@@ -191,16 +216,24 @@ export class DingTalkService {
|
|||||||
// ═══════════════════════════════════════════
|
// ═══════════════════════════════════════════
|
||||||
|
|
||||||
private async getAccessToken(): Promise<string> {
|
private async getAccessToken(): Promise<string> {
|
||||||
if (this.accessToken && Date.now() < this.tokenExpiresAt - 60_000) {
|
const credentials = await this.getCredentials();
|
||||||
|
if (!credentials) {
|
||||||
|
throw new Error('DingTalk not configured');
|
||||||
|
}
|
||||||
|
|
||||||
|
const credentialKey = `${credentials.appKey}:${credentials.appSecret}`;
|
||||||
|
if (
|
||||||
|
this.accessToken &&
|
||||||
|
this.accessTokenCredentialKey === credentialKey &&
|
||||||
|
Date.now() < this.tokenExpiresAt - 60_000
|
||||||
|
) {
|
||||||
return this.accessToken;
|
return this.accessToken;
|
||||||
}
|
}
|
||||||
const appKey = process.env.DINGTALK_APP_KEY!;
|
|
||||||
const appSecret = process.env.DINGTALK_APP_SECRET!;
|
|
||||||
|
|
||||||
const res = await fetch('https://api.dingtalk.com/v1.0/oauth2/accessToken', {
|
const res = await fetch('https://api.dingtalk.com/v1.0/oauth2/accessToken', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({ appKey, appSecret }),
|
body: JSON.stringify(credentials),
|
||||||
});
|
});
|
||||||
const body: DingTalkTokenResponse = await res.json();
|
const body: DingTalkTokenResponse = await res.json();
|
||||||
|
|
||||||
@@ -209,6 +242,7 @@ export class DingTalkService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.accessToken = body.accessToken;
|
this.accessToken = body.accessToken;
|
||||||
|
this.accessTokenCredentialKey = credentialKey;
|
||||||
this.tokenExpiresAt = Date.now() + (body.expireIn || 7200) * 1000;
|
this.tokenExpiresAt = Date.now() + (body.expireIn || 7200) * 1000;
|
||||||
this.logger.log('钉钉 access_token 获取成功');
|
this.logger.log('钉钉 access_token 获取成功');
|
||||||
return this.accessToken;
|
return this.accessToken;
|
||||||
@@ -259,7 +293,7 @@ export class DingTalkService {
|
|||||||
// ═══════════════════════════════════════════
|
// ═══════════════════════════════════════════
|
||||||
|
|
||||||
async syncAll(rootDeptId = 1): Promise<{ deptCount: number; userCount: number }> {
|
async syncAll(rootDeptId = 1): Promise<{ deptCount: number; userCount: number }> {
|
||||||
if (!this.configured) {
|
if (!(await this.isConfigured())) {
|
||||||
this.logger.warn('钉钉未配置 (DINGTALK_APP_KEY / DINGTALK_APP_SECRET),跳过同步');
|
this.logger.warn('钉钉未配置 (DINGTALK_APP_KEY / DINGTALK_APP_SECRET),跳过同步');
|
||||||
return { deptCount: 0, userCount: 0 };
|
return { deptCount: 0, userCount: 0 };
|
||||||
}
|
}
|
||||||
@@ -333,7 +367,7 @@ export class DingTalkService {
|
|||||||
|
|
||||||
/** 获取钉钉组织部门树(只含部门) */
|
/** 获取钉钉组织部门树(只含部门) */
|
||||||
async fetchOrgTree(rootDeptId = 1): Promise<OrgDeptNode[]> {
|
async fetchOrgTree(rootDeptId = 1): Promise<OrgDeptNode[]> {
|
||||||
if (!this.configured) return [];
|
if (!(await this.isConfigured())) return [];
|
||||||
const token = await this.getAccessToken();
|
const token = await this.getAccessToken();
|
||||||
const rootInfo = await this.getDeptInfo(token, rootDeptId);
|
const rootInfo = await this.getDeptInfo(token, rootDeptId);
|
||||||
if (!rootInfo) return [];
|
if (!rootInfo) return [];
|
||||||
@@ -343,7 +377,7 @@ export class DingTalkService {
|
|||||||
|
|
||||||
/** 获取钉钉组织部门树(含用户) */
|
/** 获取钉钉组织部门树(含用户) */
|
||||||
async fetchOrgTreeWithUsers(rootDeptId = 1): Promise<OrgDeptNodeWithUsers[]> {
|
async fetchOrgTreeWithUsers(rootDeptId = 1): Promise<OrgDeptNodeWithUsers[]> {
|
||||||
if (!this.configured) return [];
|
if (!(await this.isConfigured())) return [];
|
||||||
const token = await this.getAccessToken();
|
const token = await this.getAccessToken();
|
||||||
const rootInfo = await this.getDeptInfo(token, rootDeptId);
|
const rootInfo = await this.getDeptInfo(token, rootDeptId);
|
||||||
if (!rootInfo) return [];
|
if (!rootInfo) return [];
|
||||||
@@ -441,7 +475,7 @@ export class DingTalkService {
|
|||||||
endDate: string;
|
endDate: string;
|
||||||
userIds?: string[];
|
userIds?: string[];
|
||||||
}): Promise<DingTalkAttendanceResult[]> {
|
}): Promise<DingTalkAttendanceResult[]> {
|
||||||
if (!this.configured) throw new Error('DingTalk not configured');
|
if (!(await this.isConfigured())) throw new Error('DingTalk not configured');
|
||||||
if (!params.userIds?.length) throw new Error('钉钉考勤 userIds 不能为空');
|
if (!params.userIds?.length) throw new Error('钉钉考勤 userIds 不能为空');
|
||||||
if (params.userIds.length > 50) throw new Error('钉钉考勤单次最多查询50人');
|
if (params.userIds.length > 50) throw new Error('钉钉考勤单次最多查询50人');
|
||||||
const token = await this.getAccessToken();
|
const token = await this.getAccessToken();
|
||||||
@@ -496,7 +530,7 @@ export class DingTalkService {
|
|||||||
|
|
||||||
/** 创建或修改班次。id 不传=创建,传了=修改 */
|
/** 创建或修改班次。id 不传=创建,传了=修改 */
|
||||||
async upsertShift(params: DingTalkShiftParams): Promise<number> {
|
async upsertShift(params: DingTalkShiftParams): Promise<number> {
|
||||||
if (!this.configured) throw new ServiceUnavailableException('钉钉未配置');
|
if (!(await this.isConfigured())) throw new ServiceUnavailableException('钉钉未配置');
|
||||||
const token = await this.getAccessToken();
|
const token = await this.getAccessToken();
|
||||||
|
|
||||||
const body: Record<string, unknown> = {
|
const body: Record<string, unknown> = {
|
||||||
@@ -547,7 +581,7 @@ export class DingTalkService {
|
|||||||
|
|
||||||
/** 查询所有班次摘要(每页最多200条) */
|
/** 查询所有班次摘要(每页最多200条) */
|
||||||
async queryShifts(opUserId = 'manager'): Promise<DingTalkShiftSummary[]> {
|
async queryShifts(opUserId = 'manager'): Promise<DingTalkShiftSummary[]> {
|
||||||
if (!this.configured) throw new ServiceUnavailableException('钉钉未配置');
|
if (!(await this.isConfigured())) throw new ServiceUnavailableException('钉钉未配置');
|
||||||
const token = await this.getAccessToken();
|
const token = await this.getAccessToken();
|
||||||
|
|
||||||
const all: DingTalkShiftSummary[] = [];
|
const all: DingTalkShiftSummary[] = [];
|
||||||
@@ -598,7 +632,7 @@ export class DingTalkService {
|
|||||||
|
|
||||||
/** 创建排班制考勤组 */
|
/** 创建排班制考勤组 */
|
||||||
async createAttendanceGroup(params: DingTalkGroupParams): Promise<number> {
|
async createAttendanceGroup(params: DingTalkGroupParams): Promise<number> {
|
||||||
if (!this.configured) throw new ServiceUnavailableException('钉钉未配置');
|
if (!(await this.isConfigured())) throw new ServiceUnavailableException('钉钉未配置');
|
||||||
const token = await this.getAccessToken();
|
const token = await this.getAccessToken();
|
||||||
|
|
||||||
const topGroup = this.buildAttendanceGroupBody(params);
|
const topGroup = this.buildAttendanceGroupBody(params);
|
||||||
@@ -627,7 +661,7 @@ export class DingTalkService {
|
|||||||
|
|
||||||
/** 更新排班制考勤组,确保复用考勤组时同步最新打卡限制 */
|
/** 更新排班制考勤组,确保复用考勤组时同步最新打卡限制 */
|
||||||
async updateAttendanceGroup(params: DingTalkGroupUpdateParams): Promise<void> {
|
async updateAttendanceGroup(params: DingTalkGroupUpdateParams): Promise<void> {
|
||||||
if (!this.configured) throw new ServiceUnavailableException('钉钉未配置');
|
if (!(await this.isConfigured())) throw new ServiceUnavailableException('钉钉未配置');
|
||||||
const token = await this.getAccessToken();
|
const token = await this.getAccessToken();
|
||||||
const topGroup = { ...this.buildAttendanceGroupBody(params), id: params.id };
|
const topGroup = { ...this.buildAttendanceGroupBody(params), id: params.id };
|
||||||
|
|
||||||
@@ -687,7 +721,7 @@ export class DingTalkService {
|
|||||||
|
|
||||||
/** 查询所有考勤组摘要(分页,每页10条) */
|
/** 查询所有考勤组摘要(分页,每页10条) */
|
||||||
async queryAttendanceGroups(opUserId = 'manager'): Promise<DingTalkGroupSummary[]> {
|
async queryAttendanceGroups(opUserId = 'manager'): Promise<DingTalkGroupSummary[]> {
|
||||||
if (!this.configured) throw new ServiceUnavailableException('钉钉未配置');
|
if (!(await this.isConfigured())) throw new ServiceUnavailableException('钉钉未配置');
|
||||||
const token = await this.getAccessToken();
|
const token = await this.getAccessToken();
|
||||||
|
|
||||||
const all: DingTalkGroupSummary[] = [];
|
const all: DingTalkGroupSummary[] = [];
|
||||||
@@ -729,7 +763,7 @@ export class DingTalkService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async deleteAttendanceGroup(groupId: number, opUserId = 'manager'): Promise<void> {
|
async deleteAttendanceGroup(groupId: number, opUserId = 'manager'): Promise<void> {
|
||||||
if (!this.configured) throw new ServiceUnavailableException('钉钉未配置');
|
if (!(await this.isConfigured())) throw new ServiceUnavailableException('钉钉未配置');
|
||||||
const token = await this.getAccessToken();
|
const token = await this.getAccessToken();
|
||||||
|
|
||||||
await this.rateLimit();
|
await this.rateLimit();
|
||||||
@@ -778,7 +812,7 @@ export class DingTalkService {
|
|||||||
async scheduleUsers(
|
async scheduleUsers(
|
||||||
groupId: number, schedules: DingTalkScheduleItem[], opUserId = 'manager',
|
groupId: number, schedules: DingTalkScheduleItem[], opUserId = 'manager',
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
if (!this.configured) throw new ServiceUnavailableException('钉钉未配置');
|
if (!(await this.isConfigured())) throw new ServiceUnavailableException('钉钉未配置');
|
||||||
if (schedules.length === 0) return;
|
if (schedules.length === 0) return;
|
||||||
if (schedules.length > 200) {
|
if (schedules.length > 200) {
|
||||||
throw new Error(`排班单次最多200条,当前 ${schedules.length} 条`);
|
throw new Error(`排班单次最多200条,当前 ${schedules.length} 条`);
|
||||||
@@ -818,7 +852,7 @@ export class DingTalkService {
|
|||||||
async queryScheduleByUsers(
|
async queryScheduleByUsers(
|
||||||
userIds: string[], fromDate: number, toDate: number, opUserId = 'manager',
|
userIds: string[], fromDate: number, toDate: number, opUserId = 'manager',
|
||||||
): Promise<DingTalkScheduleResult[]> {
|
): Promise<DingTalkScheduleResult[]> {
|
||||||
if (!this.configured) throw new ServiceUnavailableException('钉钉未配置');
|
if (!(await this.isConfigured())) throw new ServiceUnavailableException('钉钉未配置');
|
||||||
const token = await this.getAccessToken();
|
const token = await this.getAccessToken();
|
||||||
|
|
||||||
await this.rateLimit();
|
await this.rateLimit();
|
||||||
|
|||||||
@@ -3,9 +3,13 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
|||||||
import { User, Student, StudentDingMapping, Class } from '../entities';
|
import { User, Student, StudentDingMapping, Class } from '../entities';
|
||||||
import { DingTalkService } from './dingtalk.service';
|
import { DingTalkService } from './dingtalk.service';
|
||||||
import { WeComService } from './wecom.service';
|
import { WeComService } from './wecom.service';
|
||||||
|
import { IntegrationConfigModule } from './config/config.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [TypeOrmModule.forFeature([User, Student, StudentDingMapping, Class])],
|
imports: [
|
||||||
|
TypeOrmModule.forFeature([User, Student, StudentDingMapping, Class]),
|
||||||
|
IntegrationConfigModule,
|
||||||
|
],
|
||||||
providers: [DingTalkService, WeComService],
|
providers: [DingTalkService, WeComService],
|
||||||
exports: [DingTalkService, WeComService],
|
exports: [DingTalkService, WeComService],
|
||||||
})
|
})
|
||||||
|
|||||||
26
package-lock.json
generated
26
package-lock.json
generated
@@ -98,6 +98,7 @@
|
|||||||
"@types/passport-jwt": "^4.0.1",
|
"@types/passport-jwt": "^4.0.1",
|
||||||
"@types/passport-local": "^1.0.38",
|
"@types/passport-local": "^1.0.38",
|
||||||
"@types/supertest": "^7.0.0",
|
"@types/supertest": "^7.0.0",
|
||||||
|
"cross-env": "^10.1.0",
|
||||||
"eslint": "^9.18.0",
|
"eslint": "^9.18.0",
|
||||||
"globals": "^17.0.0",
|
"globals": "^17.0.0",
|
||||||
"jest": "^30.0.0",
|
"jest": "^30.0.0",
|
||||||
@@ -1133,6 +1134,13 @@
|
|||||||
"integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==",
|
"integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/@epic-web/invariant": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@epic-web/invariant/-/invariant-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@eslint-community/eslint-utils": {
|
"node_modules/@eslint-community/eslint-utils": {
|
||||||
"version": "4.9.1",
|
"version": "4.9.1",
|
||||||
"resolved": "https://registry.npmmirror.com/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
|
"resolved": "https://registry.npmmirror.com/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz",
|
||||||
@@ -8697,6 +8705,24 @@
|
|||||||
"url": "https://ko-fi.com/intcreator"
|
"url": "https://ko-fi.com/intcreator"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/cross-env": {
|
||||||
|
"version": "10.1.0",
|
||||||
|
"resolved": "https://registry.npmmirror.com/cross-env/-/cross-env-10.1.0.tgz",
|
||||||
|
"integrity": "sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@epic-web/invariant": "^1.0.0",
|
||||||
|
"cross-spawn": "^7.0.6"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"cross-env": "dist/bin/cross-env.js",
|
||||||
|
"cross-env-shell": "dist/bin/cross-env-shell.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/cross-spawn": {
|
"node_modules/cross-spawn": {
|
||||||
"version": "7.0.6",
|
"version": "7.0.6",
|
||||||
"resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
"resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||||
|
|||||||
Reference in New Issue
Block a user