updated dependencies
This commit is contained in:
parent
d081be19a1
commit
7bfdae0519
43
backend/package-lock.json
generated
43
backend/package-lock.json
generated
@ -20,7 +20,6 @@
|
||||
"express": "^4.22.2",
|
||||
"ffmpeg-static": "^5.2.0",
|
||||
"ffprobe-static": "^3.1.0",
|
||||
"fluent-ffmpeg": "^2.1.3",
|
||||
"helmet": "^8.0.0",
|
||||
"joi": "^17.13.0",
|
||||
"json2csv": "^5.0.7",
|
||||
@ -47,7 +46,6 @@
|
||||
"@types/express": "^4.17.25",
|
||||
"@types/express-serve-static-core": "^4.19.8",
|
||||
"@types/ffprobe-static": "^2.0.3",
|
||||
"@types/fluent-ffmpeg": "^2.1.28",
|
||||
"@types/json2csv": "^5.0.7",
|
||||
"@types/jsonwebtoken": "^9.0.10",
|
||||
"@types/multer": "^2.1.0",
|
||||
@ -2300,16 +2298,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/fluent-ffmpeg": {
|
||||
"version": "2.1.28",
|
||||
"resolved": "https://registry.npmjs.org/@types/fluent-ffmpeg/-/fluent-ffmpeg-2.1.28.tgz",
|
||||
"integrity": "sha512-5ovxsDwBcPfJ+eYs1I/ZpcYCnkce7pvH9AHSvrZllAp1ZPpTRDZAFjF3TRFbukxSgIYTTNYePbS0rKUmaxVbXw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/http-errors": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz",
|
||||
@ -3517,11 +3505,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/async": {
|
||||
"version": "0.2.10",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz",
|
||||
"integrity": "sha512-eAkdoKxU6/LkKDBzLpT+t6Ff5EtfSF4wx1WfJiPEEV7WNLnDaRXk0oVysiEPm262roaachGexwUv94WhSgN5TQ=="
|
||||
},
|
||||
"node_modules/async-function": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz",
|
||||
@ -5538,19 +5521,6 @@
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/fluent-ffmpeg": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fluent-ffmpeg/-/fluent-ffmpeg-2.1.3.tgz",
|
||||
"integrity": "sha512-Be3narBNt2s6bsaqP6Jzq91heDgOEaDCJAXcE3qcma/EJBSy5FB4cvO31XBInuAuKBx8Kptf8dkhjK0IOru39Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"async": "^0.2.9",
|
||||
"which": "^1.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/for-each": {
|
||||
"version": "0.3.3",
|
||||
"resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
|
||||
@ -6667,6 +6637,7 @@
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/jju": {
|
||||
@ -9831,18 +9802,6 @@
|
||||
"webidl-conversions": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
|
||||
"integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"isexe": "^2.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"which": "bin/which"
|
||||
}
|
||||
},
|
||||
"node_modules/which-boxed-primitive": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz",
|
||||
|
||||
@ -43,7 +43,6 @@
|
||||
"express": "^4.22.2",
|
||||
"ffmpeg-static": "^5.2.0",
|
||||
"ffprobe-static": "^3.1.0",
|
||||
"fluent-ffmpeg": "^2.1.3",
|
||||
"helmet": "^8.0.0",
|
||||
"joi": "^17.13.0",
|
||||
"json2csv": "^5.0.7",
|
||||
@ -85,7 +84,6 @@
|
||||
"@types/express": "^4.17.25",
|
||||
"@types/express-serve-static-core": "^4.19.8",
|
||||
"@types/ffprobe-static": "^2.0.3",
|
||||
"@types/fluent-ffmpeg": "^2.1.28",
|
||||
"@types/json2csv": "^5.0.7",
|
||||
"@types/jsonwebtoken": "^9.0.10",
|
||||
"@types/multer": "^2.1.0",
|
||||
|
||||
@ -6,11 +6,11 @@
|
||||
*/
|
||||
|
||||
import { promises as fs } from 'fs';
|
||||
import { spawn } from 'node:child_process';
|
||||
import { createRequire } from 'node:module';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
|
||||
import ffmpeg from 'fluent-ffmpeg';
|
||||
import { path as ffprobePath } from 'ffprobe-static';
|
||||
|
||||
import config from '../config.ts';
|
||||
@ -22,11 +22,6 @@ const loadCommonJsModule = createRequire(import.meta.url);
|
||||
const ffmpegStaticValue: unknown = loadCommonJsModule('ffmpeg-static');
|
||||
const ffmpegPath = typeof ffmpegStaticValue === 'string' ? ffmpegStaticValue : null;
|
||||
|
||||
if (ffmpegPath) {
|
||||
ffmpeg.setFfmpegPath(ffmpegPath);
|
||||
}
|
||||
ffmpeg.setFfprobePath(ffprobePath);
|
||||
|
||||
let ffmpegQueueTail: Promise<unknown> = Promise.resolve();
|
||||
let queuedFfmpegJobs = 0;
|
||||
let ffmpegJobSequence = 0;
|
||||
@ -41,6 +36,27 @@ const reverseVideoBreaker = new CircuitBreaker({
|
||||
const FFMPEG_REVERSE_TIMEOUT_MS = config.resilience.ffmpeg.reverseTimeoutMs;
|
||||
const FFPROBE_TIMEOUT_MS = config.resilience.ffmpeg.ffprobeTimeoutMs;
|
||||
|
||||
interface MediaProbeStream {
|
||||
codec_type?: string | undefined;
|
||||
duration?: string | number | undefined;
|
||||
width?: string | number | undefined;
|
||||
height?: string | number | undefined;
|
||||
avg_frame_rate?: string | number | undefined;
|
||||
r_frame_rate?: string | number | undefined;
|
||||
}
|
||||
|
||||
interface MediaProbeOutput {
|
||||
streams?: MediaProbeStream[] | undefined;
|
||||
format?: {
|
||||
duration?: string | number | undefined;
|
||||
} | undefined;
|
||||
}
|
||||
|
||||
interface ProcessResult {
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
}
|
||||
|
||||
function parseFrameRate(value: unknown): number | null {
|
||||
if (!value) {
|
||||
return null;
|
||||
@ -86,6 +102,75 @@ function toError(value: unknown): Error {
|
||||
return value instanceof Error ? value : new Error(String(value));
|
||||
}
|
||||
|
||||
function isMediaProbeOutput(value: unknown): value is MediaProbeOutput {
|
||||
return Boolean(value) && typeof value === 'object';
|
||||
}
|
||||
|
||||
function getFfmpegExecutablePath(): string {
|
||||
if (!ffmpegPath) {
|
||||
throw new Error('Bundled FFmpeg binary is unavailable');
|
||||
}
|
||||
return ffmpegPath;
|
||||
}
|
||||
|
||||
async function runProcess(
|
||||
executablePath: string,
|
||||
args: string[],
|
||||
options: { timeoutMs: number; timeoutMessage: string },
|
||||
): Promise<ProcessResult> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn(executablePath, args, {
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
});
|
||||
|
||||
let stdout = '';
|
||||
let stderr = '';
|
||||
let settled = false;
|
||||
|
||||
const finish = (error: Error | null, result?: ProcessResult): void => {
|
||||
if (settled) {
|
||||
return;
|
||||
}
|
||||
settled = true;
|
||||
clearTimeout(timeout);
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
resolve(result ?? { stdout, stderr });
|
||||
};
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
const error = new Error(options.timeoutMessage);
|
||||
child.kill('SIGKILL');
|
||||
finish(error);
|
||||
}, options.timeoutMs);
|
||||
timeout.unref();
|
||||
|
||||
child.stdout.setEncoding('utf8');
|
||||
child.stderr.setEncoding('utf8');
|
||||
child.stdout.on('data', (chunk: string) => {
|
||||
stdout += chunk;
|
||||
});
|
||||
child.stderr.on('data', (chunk: string) => {
|
||||
stderr += chunk;
|
||||
});
|
||||
child.on('error', (error) => finish(error));
|
||||
child.on('close', (code, signal) => {
|
||||
if (code === 0) {
|
||||
finish(null, { stdout, stderr });
|
||||
return;
|
||||
}
|
||||
|
||||
finish(
|
||||
new Error(
|
||||
`Process failed: ${executablePath} exited with code ${code ?? 'null'} and signal ${signal ?? 'null'}`,
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function enqueueFfmpegJob<TResult>(
|
||||
jobName: string,
|
||||
runJob: FfmpegJobRunner<TResult>,
|
||||
@ -167,59 +252,44 @@ async function reverseVideoWithoutQueue(
|
||||
'Starting video reversal',
|
||||
);
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const command = ffmpeg(inputPath)
|
||||
.outputOptions([
|
||||
'-vf',
|
||||
'reverse',
|
||||
'-af',
|
||||
'areverse',
|
||||
'-c:v',
|
||||
'libx264',
|
||||
'-preset',
|
||||
'fast',
|
||||
'-crf',
|
||||
'23',
|
||||
'-c:a',
|
||||
'aac',
|
||||
'-threads',
|
||||
'1',
|
||||
'-movflags',
|
||||
'+faststart',
|
||||
])
|
||||
.output(outputPath)
|
||||
.on('start', (cmd) => logger.debug({ cmd }, 'FFmpeg command'))
|
||||
.on('progress', (progress) => {
|
||||
if (progress.percent) {
|
||||
logger.debug({ percent: progress.percent }, 'FFmpeg progress');
|
||||
}
|
||||
})
|
||||
.on('end', () => {
|
||||
clearTimeout(timeout);
|
||||
logger.info({ outputPath }, 'Video reversal complete');
|
||||
resolve();
|
||||
})
|
||||
.on('error', (err, stdout, stderr) => {
|
||||
clearTimeout(timeout);
|
||||
logger.error({ err, stdout, stderr }, 'FFmpeg error');
|
||||
reject(err);
|
||||
});
|
||||
const ffmpegArgs = [
|
||||
'-y',
|
||||
'-i',
|
||||
inputPath,
|
||||
'-vf',
|
||||
'reverse',
|
||||
'-af',
|
||||
'areverse',
|
||||
'-c:v',
|
||||
'libx264',
|
||||
'-preset',
|
||||
'fast',
|
||||
'-crf',
|
||||
'23',
|
||||
'-c:a',
|
||||
'aac',
|
||||
'-threads',
|
||||
'1',
|
||||
'-movflags',
|
||||
'+faststart',
|
||||
outputPath,
|
||||
];
|
||||
|
||||
const timeout = setTimeout(() => {
|
||||
const error = new Error(
|
||||
`FFmpeg reverse video timed out after ${FFMPEG_REVERSE_TIMEOUT_MS}ms`,
|
||||
);
|
||||
logger.error(
|
||||
{ err: error, inputPath, outputPath },
|
||||
'FFmpeg reverse video timeout',
|
||||
);
|
||||
command.kill('SIGKILL');
|
||||
reject(error);
|
||||
}, FFMPEG_REVERSE_TIMEOUT_MS);
|
||||
timeout.unref();
|
||||
logger.debug(
|
||||
{ cmd: [getFfmpegExecutablePath(), ...ffmpegArgs].join(' ') },
|
||||
'FFmpeg command',
|
||||
);
|
||||
|
||||
command.run();
|
||||
});
|
||||
try {
|
||||
await runProcess(getFfmpegExecutablePath(), ffmpegArgs, {
|
||||
timeoutMs: FFMPEG_REVERSE_TIMEOUT_MS,
|
||||
timeoutMessage: `FFmpeg reverse video timed out after ${FFMPEG_REVERSE_TIMEOUT_MS}ms`,
|
||||
});
|
||||
logger.info({ outputPath }, 'Video reversal complete');
|
||||
} catch (err) {
|
||||
logger.error({ err, inputPath, outputPath }, 'FFmpeg error');
|
||||
throw err;
|
||||
}
|
||||
|
||||
const reversedBuffer = await fs.readFile(outputPath);
|
||||
let outputMetadata: MediaMetadata | null = null;
|
||||
@ -253,59 +323,74 @@ async function reverseVideoWithoutQueue(
|
||||
}
|
||||
|
||||
async function probeMediaMetadata(filePath: string): Promise<MediaMetadata> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timeout = setTimeout(() => {
|
||||
reject(
|
||||
new Error(`FFprobe timed out after ${FFPROBE_TIMEOUT_MS}ms: ${filePath}`),
|
||||
);
|
||||
}, FFPROBE_TIMEOUT_MS);
|
||||
timeout.unref();
|
||||
const result = await runProcess(
|
||||
ffprobePath,
|
||||
[
|
||||
'-v',
|
||||
'error',
|
||||
'-print_format',
|
||||
'json',
|
||||
'-show_format',
|
||||
'-show_streams',
|
||||
filePath,
|
||||
],
|
||||
{
|
||||
timeoutMs: FFPROBE_TIMEOUT_MS,
|
||||
timeoutMessage: `FFprobe timed out after ${FFPROBE_TIMEOUT_MS}ms: ${filePath}`,
|
||||
},
|
||||
);
|
||||
|
||||
ffmpeg.ffprobe(filePath, (err: unknown, metadata) => {
|
||||
clearTimeout(timeout);
|
||||
if (err) {
|
||||
reject(toError(err));
|
||||
return;
|
||||
}
|
||||
let metadata: MediaProbeOutput;
|
||||
try {
|
||||
const parsedMetadata: unknown = JSON.parse(result.stdout);
|
||||
if (!isMediaProbeOutput(parsedMetadata)) {
|
||||
throw new Error('FFprobe output is not a JSON object');
|
||||
}
|
||||
metadata = parsedMetadata;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to parse FFprobe metadata: ${toError(error).message}`);
|
||||
}
|
||||
|
||||
const videoStream =
|
||||
metadata.streams.find((stream) => stream.codec_type === 'video') ??
|
||||
null;
|
||||
const audioStream =
|
||||
metadata.streams.find((stream) => stream.codec_type === 'audio') ??
|
||||
null;
|
||||
const primaryStream = videoStream ?? audioStream;
|
||||
const formatDuration = Number(metadata.format.duration);
|
||||
const streamDuration = Number(primaryStream?.duration);
|
||||
const durationSec =
|
||||
Number.isFinite(formatDuration) && formatDuration > 0
|
||||
? formatDuration
|
||||
: Number.isFinite(streamDuration) && streamDuration > 0
|
||||
? streamDuration
|
||||
: null;
|
||||
const streams = metadata.streams ?? [];
|
||||
const videoStream =
|
||||
streams.find((stream) => stream.codec_type === 'video') ?? null;
|
||||
const audioStream =
|
||||
streams.find((stream) => stream.codec_type === 'audio') ?? null;
|
||||
const primaryStream = videoStream ?? audioStream;
|
||||
const formatDuration = Number(metadata.format?.duration);
|
||||
const streamDuration = Number(primaryStream?.duration);
|
||||
const durationSec =
|
||||
Number.isFinite(formatDuration) && formatDuration > 0
|
||||
? formatDuration
|
||||
: Number.isFinite(streamDuration) && streamDuration > 0
|
||||
? streamDuration
|
||||
: null;
|
||||
|
||||
const widthPx = Number(videoStream?.width);
|
||||
const heightPx = Number(videoStream?.height);
|
||||
const frameRate =
|
||||
parseFrameRate(videoStream?.avg_frame_rate) ??
|
||||
parseFrameRate(videoStream?.r_frame_rate);
|
||||
const widthPx = Number(videoStream?.width);
|
||||
const heightPx = Number(videoStream?.height);
|
||||
const frameRate =
|
||||
parseFrameRate(videoStream?.avg_frame_rate) ??
|
||||
parseFrameRate(videoStream?.r_frame_rate);
|
||||
|
||||
resolve({
|
||||
durationSec,
|
||||
widthPx: Number.isFinite(widthPx) && widthPx > 0 ? widthPx : null,
|
||||
heightPx: Number.isFinite(heightPx) && heightPx > 0 ? heightPx : null,
|
||||
frameRate,
|
||||
});
|
||||
});
|
||||
});
|
||||
return {
|
||||
durationSec,
|
||||
widthPx: Number.isFinite(widthPx) && widthPx > 0 ? widthPx : null,
|
||||
heightPx: Number.isFinite(heightPx) && heightPx > 0 ? heightPx : null,
|
||||
frameRate,
|
||||
};
|
||||
}
|
||||
|
||||
async function isFFmpegAvailable(): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
ffmpeg.getAvailableFormats((err: unknown) => {
|
||||
resolve(!err);
|
||||
try {
|
||||
await runProcess(getFfmpegExecutablePath(), ['-hide_banner', '-formats'], {
|
||||
timeoutMs: FFPROBE_TIMEOUT_MS,
|
||||
timeoutMessage: `FFmpeg availability check timed out after ${FFPROBE_TIMEOUT_MS}ms`,
|
||||
});
|
||||
});
|
||||
return true;
|
||||
} catch (err) {
|
||||
logger.warn({ err }, 'FFmpeg availability check failed');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export { isFFmpegAvailable, probeMediaMetadata, reverseVideo };
|
||||
|
||||
@ -294,8 +294,8 @@ goal is only to restore the web app. Use PM2/systemd for process recovery.
|
||||
|
||||
## Node Version
|
||||
|
||||
The project requirement is Node.js 20.x LTS. Some standard VMs may report
|
||||
`/usr/bin/node` as Node 22 in PM2. If startup fails after a system update,
|
||||
The project requirement is Node.js 24.x LTS. Some standard VMs may report
|
||||
a different `/usr/bin/node` in PM2. If startup fails after a system update,
|
||||
verify:
|
||||
|
||||
```bash
|
||||
|
||||
@ -42,53 +42,6 @@ TODO:
|
||||
- Если нужен, оставить `localStorage`, но осознанно и с коротким TTL/refresh policy.
|
||||
- Заменить frontend `jsonwebtoken` decode на `jwt-decode` или `/auth/me`.
|
||||
|
||||
## P1 - Build и зависимости
|
||||
|
||||
### Package manager
|
||||
|
||||
Решение: для root/frontend/backend используется только `npm`.
|
||||
Источник истины для dependency graph: `package-lock.json` в `frontend/` и `backend/`.
|
||||
Команды установки/deploy остаются на `npm ci` / `npm install` и `npm run ...`.
|
||||
|
||||
TODO:
|
||||
|
||||
- [x] Выбрать один package manager для frontend/backend: `npm`.
|
||||
- [x] Удалить лишний lockfile после проверки: лишних `yarn.lock` / `pnpm-lock.yaml` нет, остаются только `package-lock.json`.
|
||||
- [x] Обновить docs/deployment commands: текущие docs уже используют `npm`.
|
||||
|
||||
### Dependencies LTS alignment и obsolete package replacement
|
||||
|
||||
Зависимости нужно обновлять осознанно, чтобы не закреплять старые toolchain limitations и устаревшие библиотеки.
|
||||
|
||||
TODO:
|
||||
|
||||
- Проверить текущие frontend/backend dependencies и devDependencies на совместимость с выбранным Node LTS.
|
||||
- Обновить TypeScript, ESLint, `@typescript-eslint/*`, Next.js/React tooling и backend runtime tooling до актуальных LTS/stable versions.
|
||||
- Найти deprecated, unsupported и obsolete packages через package manager audit/outdated/deprecation warnings, npm metadata, release notes и project documentation.
|
||||
- Для deprecated/unsupported/obsolete packages выбрать actively maintained mainstream alternatives с широкой adoption, свежими releases, нормальной TypeScript/ESM support и понятным migration path.
|
||||
- Для каждой замены зафиксировать причину: что именно deprecated/unsupported/obsolete, какой replacement выбран, какие breaking changes ожидаются.
|
||||
- Проверить DB scripts, migrations/seeders, test runner и production start после dependency updates.
|
||||
- Не делать blind major upgrades без changelog/release notes и rollback plan.
|
||||
|
||||
### Node version
|
||||
|
||||
TODO:
|
||||
|
||||
- Зафиксировать выбранный Node LTS через `backend/package.json` `engines.node`.
|
||||
- Согласовать `engines.node`.
|
||||
- Обновить `@types/node` во frontend, когда будет удобно.
|
||||
|
||||
### CI/minimal checks
|
||||
|
||||
Для небольшого проекта достаточно минимального gate:
|
||||
|
||||
- frontend build;
|
||||
- frontend lint/typecheck, если проходит;
|
||||
- backend lint;
|
||||
- smoke checklist перед deploy.
|
||||
|
||||
Audit/outdated/license checks делать периодически, не обязательно blocking на каждый PR.
|
||||
|
||||
## P2 - Security hardening
|
||||
|
||||
### CSP
|
||||
@ -101,12 +54,6 @@ TODO:
|
||||
- Если будет время, включить report-only CSP на stage.
|
||||
- Enforced CSP делать только после проверки runtime presentations.
|
||||
|
||||
## P2 - Документация
|
||||
|
||||
TODO:
|
||||
|
||||
- Обновить docs только для реально изменённых workflows/API/schema.
|
||||
|
||||
## Рекомендуемый порядок
|
||||
|
||||
1. Добавить validation для самых рискованных endpoints и запретить новые routes без validation.
|
||||
|
||||
20
frontend/package-lock.json
generated
20
frontend/package-lock.json
generated
@ -47,7 +47,7 @@
|
||||
"@tailwindcss/forms": "^0.5.7",
|
||||
"@types/file-saver": "^2.0.7",
|
||||
"@types/lodash": "^4.17.24",
|
||||
"@types/node": "18.7.16",
|
||||
"@types/node": "^24.13.2",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@typescript-eslint/eslint-plugin": "^8.62.1",
|
||||
"@typescript-eslint/parser": "^8.62.1",
|
||||
@ -2610,11 +2610,14 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "18.7.16",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.16.tgz",
|
||||
"integrity": "sha512-EQHhixfu+mkqHMZl1R2Ovuvn47PUw18azMJOTwSZr9/fhzHNGXAJ0ma0dayRVchprpCj0Kc1K1xKoWaATWF1qg==",
|
||||
"version": "24.13.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.2.tgz",
|
||||
"integrity": "sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~7.18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/parse-json": {
|
||||
"version": "4.0.2",
|
||||
@ -9065,6 +9068,13 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "7.18.2",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz",
|
||||
"integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/unrs-resolver": {
|
||||
"version": "1.12.2",
|
||||
"resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.12.2.tgz",
|
||||
|
||||
@ -60,7 +60,7 @@
|
||||
"@tailwindcss/forms": "^0.5.7",
|
||||
"@types/file-saver": "^2.0.7",
|
||||
"@types/lodash": "^4.17.24",
|
||||
"@types/node": "18.7.16",
|
||||
"@types/node": "^24.13.2",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@typescript-eslint/eslint-plugin": "^8.62.1",
|
||||
"@typescript-eslint/parser": "^8.62.1",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user