fix(uploads): hash files without WebAssembly under strict CSP
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 3m17s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 3m12s
ci / docker / docker (образы собираются — их сервер собирает сам при обновлении) (push) Successful in 1m42s
ci / ts / ts (packages + web) (push) Failing after 4m37s
ci / module boundaries / boundaries (push) Successful in 5s
ci / desktop / rust (desktop proxy) (push) Successful in 6m12s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 3m17s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 3m12s
ci / docker / docker (образы собираются — их сервер собирает сам при обновлении) (push) Successful in 1m42s
ci / ts / ts (packages + web) (push) Failing after 4m37s
ci / module boundaries / boundaries (push) Successful in 5s
ci / desktop / rust (desktop proxy) (push) Successful in 6m12s
This commit is contained in:
1 parent
f59d1e8d03
commit
b39e4dc28f
7 files changed
+58
-45
No files matched your search
@@ -13,7 +13,7 @@
|
|||||||
"withGlobalTauri": true,
|
"withGlobalTauri": true,
|
||||||
"windows": [{ "label": "main", "title": "Pipu Music", "width": 1200, "height": 800, "minWidth": 720, "minHeight": 540 }],
|
"windows": [{ "label": "main", "title": "Pipu Music", "width": 1200, "height": 800, "minWidth": 720, "minHeight": 540 }],
|
||||||
"security": {
|
"security": {
|
||||||
"csp": "default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: http://127.0.0.1:*; font-src 'self' data:; media-src 'self' blob: http://127.0.0.1:*; connect-src 'self' ipc: http://ipc.localhost http://127.0.0.1:*; object-src 'none'; base-uri 'self'; frame-src 'none'"
|
"csp": "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob: http://127.0.0.1:*; font-src 'self' data:; media-src 'self' blob: http://127.0.0.1:*; connect-src 'self' ipc: http://ipc.localhost http://127.0.0.1:*; object-src 'none'; base-uri 'self'; frame-src 'none'"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"bundle": {
|
"bundle": {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { readFileSync } from 'node:fs';
|
import { readFileSync } from 'node:fs';
|
||||||
|
import { createHash } from 'node:crypto';
|
||||||
import { expect, test } from '@playwright/test';
|
import { expect, test } from '@playwright/test';
|
||||||
|
|
||||||
const nginxHeaders = readFileSync(new URL('../../../deploy/nginx/security-headers.conf', import.meta.url), 'utf8');
|
const nginxHeaders = readFileSync(new URL('../../../deploy/nginx/security-headers.conf', import.meta.url), 'utf8');
|
||||||
@@ -20,6 +21,10 @@ for (const [platform, policy] of Object.entries(policies)) {
|
|||||||
await route.fulfill({ contentType: 'application/javascript', body: `
|
await route.fulfill({ contentType: 'application/javascript', body: `
|
||||||
try { new Function('return 1')(); document.documentElement.dataset.evalBlocked = 'false'; }
|
try { new Function('return 1')(); document.documentElement.dataset.evalBlocked = 'false'; }
|
||||||
catch { document.documentElement.dataset.evalBlocked = 'true'; }
|
catch { document.documentElement.dataset.evalBlocked = 'true'; }
|
||||||
|
WebAssembly.compile(new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0])).then(
|
||||||
|
() => { document.documentElement.dataset.wasmBlocked = 'false'; },
|
||||||
|
() => { document.documentElement.dataset.wasmBlocked = 'true'; },
|
||||||
|
);
|
||||||
` });
|
` });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -27,22 +32,37 @@ for (const [platform, policy] of Object.entries(policies)) {
|
|||||||
await route.fulfill({ response, headers: { ...response.headers(), 'Content-Security-Policy': policy } });
|
await route.fulfill({ response, headers: { ...response.headers(), 'Content-Security-Policy': policy } });
|
||||||
});
|
});
|
||||||
await page.goto('/csp-upload-test');
|
await page.goto('/csp-upload-test');
|
||||||
const result = await page.evaluate(() => new Promise((resolve, reject) => {
|
const result = await page.evaluate(async () => {
|
||||||
const worker = new Worker('/src/features/uploads/hash.worker.ts', { type: 'module' });
|
const worker = new Worker('/src/features/uploads/hash.worker.ts', { type: 'module' });
|
||||||
worker.onerror = (event) => { worker.terminate(); reject(new Error(event.message)); };
|
try {
|
||||||
worker.onmessage = ({ data }) => {
|
const results = [];
|
||||||
if (data.type === 'progress') return;
|
// Reuse one worker for empty, known-vector and multi-chunk binary files.
|
||||||
|
const binary = Uint8Array.from({ length: (9 << 20) + 17 }, (_, i) => i % 251);
|
||||||
|
for (const [id, bytes] of [['empty', new Uint8Array()], ['abc', new TextEncoder().encode('abc')], ['binary', binary]] as const) {
|
||||||
|
results.push(await new Promise((resolve, reject) => {
|
||||||
|
const progress: number[] = [];
|
||||||
|
worker.onerror = (event) => reject(new Error(event.message));
|
||||||
|
worker.onmessage = ({ data }) => {
|
||||||
|
if (data.type === 'progress') { progress.push(data.done); return; }
|
||||||
|
resolve({ ...data, progress });
|
||||||
|
};
|
||||||
|
worker.postMessage({ id, file: new File([bytes], `${id}.flac`) });
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
} finally {
|
||||||
worker.terminate();
|
worker.terminate();
|
||||||
resolve(data);
|
}
|
||||||
};
|
|
||||||
worker.postMessage({ id: 'csp-test', file: new File(['abc'], 'track.flac') });
|
|
||||||
}));
|
|
||||||
expect(result).toEqual({
|
|
||||||
id: 'csp-test', type: 'done',
|
|
||||||
sha256: 'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad',
|
|
||||||
});
|
});
|
||||||
|
const binary = Uint8Array.from({ length: (9 << 20) + 17 }, (_, i) => i % 251);
|
||||||
|
expect(result).toEqual([
|
||||||
|
{ id: 'empty', type: 'done', sha256: createHash('sha256').update('').digest('hex'), progress: [] },
|
||||||
|
{ id: 'abc', type: 'done', sha256: createHash('sha256').update('abc').digest('hex'), progress: [] },
|
||||||
|
{ id: 'binary', type: 'done', sha256: createHash('sha256').update(binary).digest('hex'), progress: expect.arrayContaining([expect.any(Number)]) },
|
||||||
|
]);
|
||||||
// A page script observes CSP; Playwright's evaluate bypasses the eval restriction.
|
// A page script observes CSP; Playwright's evaluate bypasses the eval restriction.
|
||||||
await page.addScriptTag({ url: '/csp-eval-test.js' });
|
await page.addScriptTag({ url: '/csp-eval-test.js' });
|
||||||
await expect(page.locator('html')).toHaveAttribute('data-eval-blocked', 'true');
|
await expect(page.locator('html')).toHaveAttribute('data-eval-blocked', 'true');
|
||||||
|
await expect(page.locator('html')).toHaveAttribute('data-wasm-blocked', 'true');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
"@tanstack/react-query": "^5.80.7",
|
"@tanstack/react-query": "^5.80.7",
|
||||||
"@tanstack/react-router": "^1.120.20",
|
"@tanstack/react-router": "^1.120.20",
|
||||||
"@tanstack/react-virtual": "^3.14.21",
|
"@tanstack/react-virtual": "^3.14.21",
|
||||||
"hash-wasm": "^4.12.0",
|
"js-sha256": "^1.0.0",
|
||||||
"music-metadata": "^12.2.3",
|
"music-metadata": "^12.2.3",
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0",
|
"react-dom": "^19.1.0",
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/// <reference lib="webworker" />
|
/// <reference lib="webworker" />
|
||||||
// SHA-256 файла потоком (docs/uploads.md#4): Web Crypto не умеет считать по частям, а файлы бывают по 2 ГБ —
|
// SHA-256 файла потоком (docs/uploads.md#4): Web Crypto не умеет считать по частям, а файлы бывают по 2 ГБ —
|
||||||
// hash-wasm считает чанками в воркере, не держа файл в памяти и не блокируя интерфейс.
|
// js-sha256 считает чанками в воркере без WebAssembly: работает и со строгим CSP у прокси.
|
||||||
import { createSHA256 } from 'hash-wasm';
|
import { sha256 } from 'js-sha256';
|
||||||
|
|
||||||
export interface HashRequest {
|
export interface HashRequest {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -18,8 +18,7 @@ const ctx = self as unknown as DedicatedWorkerGlobalScope;
|
|||||||
ctx.onmessage = async (e: MessageEvent<HashRequest>) => {
|
ctx.onmessage = async (e: MessageEvent<HashRequest>) => {
|
||||||
const { id, file } = e.data;
|
const { id, file } = e.data;
|
||||||
try {
|
try {
|
||||||
const h = await createSHA256();
|
const h = sha256.create();
|
||||||
h.init();
|
|
||||||
const reader = file.stream().getReader();
|
const reader = file.stream().getReader();
|
||||||
let done = 0;
|
let done = 0;
|
||||||
let lastReport = 0;
|
let lastReport = 0;
|
||||||
@@ -33,7 +32,7 @@ ctx.onmessage = async (e: MessageEvent<HashRequest>) => {
|
|||||||
ctx.postMessage({ id, type: 'progress', done } satisfies HashResponse);
|
ctx.postMessage({ id, type: 'progress', done } satisfies HashResponse);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ctx.postMessage({ id, type: 'done', sha256: h.digest('hex') } satisfies HashResponse);
|
ctx.postMessage({ id, type: 'done', sha256: h.hex() } satisfies HashResponse);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
ctx.postMessage({ id, type: 'error', message: err instanceof Error ? err.message : String(err) } satisfies HashResponse);
|
ctx.postMessage({ id, type: 'error', message: err instanceof Error ? err.message : String(err) } satisfies HashResponse);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,5 +3,4 @@
|
|||||||
add_header X-Content-Type-Options "nosniff" always;
|
add_header X-Content-Type-Options "nosniff" always;
|
||||||
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
|
||||||
add_header X-Frame-Options "DENY" always;
|
add_header X-Frame-Options "DENY" always;
|
||||||
# hash-wasm считает SHA-256 загрузок в Web Worker; разрешаем WASM без JavaScript eval.
|
add_header Content-Security-Policy "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; media-src 'self' blob:; font-src 'self' data:; connect-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'" always;
|
||||||
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; media-src 'self' blob:; font-src 'self' data:; connect-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'" always;
|
|
||||||
+2
-2
@@ -61,7 +61,7 @@
|
|||||||
|
|
||||||
| Уровень | Когда | Как |
|
| Уровень | Когда | Как |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| 1. Точная копия файла | **До загрузки** (на клиенте) и после | SHA-256 всего файла: клиент считает в браузере (Web Crypto, потоково в Web Worker) и отправляет в предпроверке. Сравнение с `media_files.sha256` и с другими загрузками в очереди. |
|
| 1. Точная копия файла | **До загрузки** (на клиенте) и после | SHA-256 всего файла: клиент считает потоково в Web Worker через `js-sha256`, без WebAssembly и загрузки всего файла в память, и отправляет в предпроверке. Сравнение с `media_files.sha256` и с другими загрузками в очереди. |
|
||||||
| 2. Совпадение метаданных | **До загрузки** (теги читаются на клиенте библиотекой `music-metadata`) и после (на сервере по `ffprobe`) | Нормализованные `artist + title` (+ `album`, если есть) и длительность ±2 с против `tracks`. |
|
| 2. Совпадение метаданных | **До загрузки** (теги читаются на клиенте библиотекой `music-metadata`) и после (на сервере по `ffprobe`) | Нормализованные `artist + title` (+ `album`, если есть) и длительность ±2 с против `tracks`. |
|
||||||
| 3. Акустический отпечаток | После загрузки | Chromaprint (`fpcalc`), сравнение с `track_fingerprints` библиотеки и очереди. Ловит тот же трек в другом формате / битрейте / с другими тегами. |
|
| 3. Акустический отпечаток | После загрузки | Chromaprint (`fpcalc`), сравнение с `track_fingerprints` библиотеки и очереди. Ловит тот же трек в другом формате / битрейте / с другими тегами. |
|
||||||
|
|
||||||
@@ -200,7 +200,7 @@
|
|||||||
- Настройки: `GET` / `PATCH /admin/settings/uploads` (ключ `uploads.settings` в `app_settings`).
|
- Настройки: `GET` / `PATCH /admin/settings/uploads` (ключ `uploads.settings` в `app_settings`).
|
||||||
- Веб (`apps/web/src/features/uploads`, `pages/uploads`, `pages/admin/UploadQueuePage.tsx`):
|
- Веб (`apps/web/src/features/uploads`, `pages/uploads`, `pages/admin/UploadQueuePage.tsx`):
|
||||||
- «Предложить треки» (`/uploads/new`): drag-and-drop файлов и папок (FileSystemEntry API, относительные пути),
|
- «Предложить треки» (`/uploads/new`): drag-and-drop файлов и папок (FileSystemEntry API, относительные пути),
|
||||||
выбор файлов / папки; SHA-256 потоком в Web Worker (`hash-wasm`); теги читает `music-metadata` и отдаёт их
|
выбор файлов / папки; SHA-256 потоком в Web Worker (`js-sha256`); теги читает `music-metadata` и отдаёт их
|
||||||
в предпроверку; `POST /uploads`; `tus-js-client` с готовым `uploadUrl`, куски по 8 МБ, 4 файла параллельно,
|
в предпроверку; `POST /uploads`; `tus-js-client` с готовым `uploadUrl`, куски по 8 МБ, 4 файла параллельно,
|
||||||
401 → обновление токена и повтор. Загрузчик — синглтон модуля: переходы по приложению не прерывают загрузку.
|
401 → обновление токена и повтор. Загрузчик — синглтон модуля: переходы по приложению не прерывают загрузку.
|
||||||
Незавершённые tus-ссылки пишутся в `localStorage`; после перезагрузки пользователь выбирает те же файлы,
|
Незавершённые tus-ссылки пишутся в `localStorage`; после перезагрузки пользователь выбирает те же файлы,
|
||||||
|
|||||||
Generated
+18
-23
@@ -122,7 +122,7 @@ importers:
|
|||||||
version: 19.0.14
|
version: 19.0.14
|
||||||
vitest:
|
vitest:
|
||||||
specifier: ^3.2.7
|
specifier: ^3.2.7
|
||||||
version: 3.2.7(@types/node@22.20.5)(jsdom@26.1.0)(lightningcss@1.27.0)(supports-color@10.2.2)(terser@5.51.2)
|
version: 3.2.7(@types/node@22.20.5)(jsdom@26.1.0)(lightningcss@1.27.0)(terser@5.51.2)
|
||||||
|
|
||||||
apps/web:
|
apps/web:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -159,9 +159,9 @@ importers:
|
|||||||
'@tanstack/react-virtual':
|
'@tanstack/react-virtual':
|
||||||
specifier: ^3.14.21
|
specifier: ^3.14.21
|
||||||
version: 3.14.21(react-dom@19.3.1(react@19.3.1))(react@19.3.1)
|
version: 3.14.21(react-dom@19.3.1(react@19.3.1))(react@19.3.1)
|
||||||
hash-wasm:
|
js-sha256:
|
||||||
specifier: ^4.12.0
|
specifier: ^1.0.0
|
||||||
version: 4.12.0
|
version: 1.0.0
|
||||||
music-metadata:
|
music-metadata:
|
||||||
specifier: ^12.2.3
|
specifier: ^12.2.3
|
||||||
version: 12.2.3(supports-color@10.2.2)
|
version: 12.2.3(supports-color@10.2.2)
|
||||||
@@ -207,13 +207,13 @@ importers:
|
|||||||
version: 4.14.0
|
version: 4.14.0
|
||||||
jsdom:
|
jsdom:
|
||||||
specifier: ^26.1.0
|
specifier: ^26.1.0
|
||||||
version: 26.1.0
|
version: 26.1.0(supports-color@10.2.2)
|
||||||
vite:
|
vite:
|
||||||
specifier: ^6.3.5
|
specifier: ^6.3.5
|
||||||
version: 6.4.4(@types/node@26.6.5)(lightningcss@1.27.0)(terser@5.51.2)
|
version: 6.4.4(@types/node@26.6.5)(lightningcss@1.27.0)(terser@5.51.2)
|
||||||
vitest:
|
vitest:
|
||||||
specifier: ^3.2.4
|
specifier: ^3.2.4
|
||||||
version: 3.2.7(@types/node@26.6.5)(jsdom@26.1.0)(lightningcss@1.27.0)(terser@5.51.2)
|
version: 3.2.7(@types/node@26.6.5)(jsdom@26.1.0(supports-color@10.2.2))(lightningcss@1.27.0)(supports-color@10.2.2)(terser@5.51.2)
|
||||||
|
|
||||||
packages/api-client:
|
packages/api-client:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -233,7 +233,7 @@ importers:
|
|||||||
devDependencies:
|
devDependencies:
|
||||||
vitest:
|
vitest:
|
||||||
specifier: ^3.2.4
|
specifier: ^3.2.4
|
||||||
version: 3.2.7(@types/node@26.6.5)(jsdom@26.1.0)(lightningcss@1.27.0)(terser@5.51.2)
|
version: 3.2.7(@types/node@26.6.5)(jsdom@26.1.0(supports-color@10.2.2))(lightningcss@1.27.0)(supports-color@10.2.2)(terser@5.51.2)
|
||||||
|
|
||||||
packages/design-tokens: {}
|
packages/design-tokens: {}
|
||||||
|
|
||||||
@@ -2479,9 +2479,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
|
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
hash-wasm@4.12.0:
|
|
||||||
resolution: {integrity: sha512-+/2B2rYLb48I/evdOIhP+K/DD2ca2fgBjp6O+GBEnCDk2e4rpeXIK8GvIyRPjTezgmWn9gmKwkQjjx6BtqDHVQ==}
|
|
||||||
|
|
||||||
hasown@2.0.4:
|
hasown@2.0.4:
|
||||||
resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==}
|
resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==}
|
||||||
engines: {node: '>= 0.4'}
|
engines: {node: '>= 0.4'}
|
||||||
@@ -6710,8 +6707,6 @@ snapshots:
|
|||||||
|
|
||||||
has-flag@4.0.0: {}
|
has-flag@4.0.0: {}
|
||||||
|
|
||||||
hash-wasm@4.12.0: {}
|
|
||||||
|
|
||||||
hasown@2.0.4:
|
hasown@2.0.4:
|
||||||
dependencies:
|
dependencies:
|
||||||
function-bind: 1.1.2
|
function-bind: 1.1.2
|
||||||
@@ -6748,7 +6743,7 @@ snapshots:
|
|||||||
statuses: 2.0.2
|
statuses: 2.0.2
|
||||||
toidentifier: 1.0.1
|
toidentifier: 1.0.1
|
||||||
|
|
||||||
http-proxy-agent@7.0.2:
|
http-proxy-agent@7.0.2(supports-color@10.2.2):
|
||||||
dependencies:
|
dependencies:
|
||||||
agent-base: 7.1.4
|
agent-base: 7.1.4
|
||||||
debug: 4.4.3(supports-color@10.2.2)
|
debug: 4.4.3(supports-color@10.2.2)
|
||||||
@@ -6943,13 +6938,13 @@ snapshots:
|
|||||||
|
|
||||||
jsc-safe-url@0.2.4: {}
|
jsc-safe-url@0.2.4: {}
|
||||||
|
|
||||||
jsdom@26.1.0:
|
jsdom@26.1.0(supports-color@10.2.2):
|
||||||
dependencies:
|
dependencies:
|
||||||
cssstyle: 4.6.0
|
cssstyle: 4.6.0
|
||||||
data-urls: 5.0.0
|
data-urls: 5.0.0
|
||||||
decimal.js: 10.6.0
|
decimal.js: 10.6.0
|
||||||
html-encoding-sniffer: 4.0.0
|
html-encoding-sniffer: 4.0.0
|
||||||
http-proxy-agent: 7.0.2
|
http-proxy-agent: 7.0.2(supports-color@10.2.2)
|
||||||
https-proxy-agent: 7.0.6(supports-color@10.2.2)
|
https-proxy-agent: 7.0.6(supports-color@10.2.2)
|
||||||
is-potential-custom-element-name: 1.0.1
|
is-potential-custom-element-name: 1.0.1
|
||||||
nwsapi: 2.2.28
|
nwsapi: 2.2.28
|
||||||
@@ -8247,7 +8242,7 @@ snapshots:
|
|||||||
|
|
||||||
vary@1.1.2: {}
|
vary@1.1.2: {}
|
||||||
|
|
||||||
vite-node@3.2.4(@types/node@22.20.5)(lightningcss@1.27.0)(supports-color@10.2.2)(terser@5.51.2):
|
vite-node@3.2.4(@types/node@22.20.5)(lightningcss@1.27.0)(terser@5.51.2):
|
||||||
dependencies:
|
dependencies:
|
||||||
cac: 6.7.14
|
cac: 6.7.14
|
||||||
debug: 4.4.3(supports-color@10.2.2)
|
debug: 4.4.3(supports-color@10.2.2)
|
||||||
@@ -8268,7 +8263,7 @@ snapshots:
|
|||||||
- tsx
|
- tsx
|
||||||
- yaml
|
- yaml
|
||||||
|
|
||||||
vite-node@3.2.4(@types/node@26.6.5)(lightningcss@1.27.0)(terser@5.51.2):
|
vite-node@3.2.4(@types/node@26.6.5)(lightningcss@1.27.0)(supports-color@10.2.2)(terser@5.51.2):
|
||||||
dependencies:
|
dependencies:
|
||||||
cac: 6.7.14
|
cac: 6.7.14
|
||||||
debug: 4.4.3(supports-color@10.2.2)
|
debug: 4.4.3(supports-color@10.2.2)
|
||||||
@@ -8317,7 +8312,7 @@ snapshots:
|
|||||||
lightningcss: 1.27.0
|
lightningcss: 1.27.0
|
||||||
terser: 5.51.2
|
terser: 5.51.2
|
||||||
|
|
||||||
vitest@3.2.7(@types/node@22.20.5)(jsdom@26.1.0)(lightningcss@1.27.0)(supports-color@10.2.2)(terser@5.51.2):
|
vitest@3.2.7(@types/node@22.20.5)(jsdom@26.1.0)(lightningcss@1.27.0)(terser@5.51.2):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/chai': 5.2.3
|
'@types/chai': 5.2.3
|
||||||
'@vitest/expect': 3.2.7
|
'@vitest/expect': 3.2.7
|
||||||
@@ -8340,11 +8335,11 @@ snapshots:
|
|||||||
tinypool: 1.1.1
|
tinypool: 1.1.1
|
||||||
tinyrainbow: 2.0.0
|
tinyrainbow: 2.0.0
|
||||||
vite: 6.4.4(@types/node@22.20.5)(lightningcss@1.27.0)(terser@5.51.2)
|
vite: 6.4.4(@types/node@22.20.5)(lightningcss@1.27.0)(terser@5.51.2)
|
||||||
vite-node: 3.2.4(@types/node@22.20.5)(lightningcss@1.27.0)(supports-color@10.2.2)(terser@5.51.2)
|
vite-node: 3.2.4(@types/node@22.20.5)(lightningcss@1.27.0)(terser@5.51.2)
|
||||||
why-is-node-running: 2.3.0
|
why-is-node-running: 2.3.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/node': 22.20.5
|
'@types/node': 22.20.5
|
||||||
jsdom: 26.1.0
|
jsdom: 26.1.0(supports-color@10.2.2)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- jiti
|
- jiti
|
||||||
- less
|
- less
|
||||||
@@ -8359,7 +8354,7 @@ snapshots:
|
|||||||
- tsx
|
- tsx
|
||||||
- yaml
|
- yaml
|
||||||
|
|
||||||
vitest@3.2.7(@types/node@26.6.5)(jsdom@26.1.0)(lightningcss@1.27.0)(terser@5.51.2):
|
vitest@3.2.7(@types/node@26.6.5)(jsdom@26.1.0(supports-color@10.2.2))(lightningcss@1.27.0)(supports-color@10.2.2)(terser@5.51.2):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/chai': 5.2.3
|
'@types/chai': 5.2.3
|
||||||
'@vitest/expect': 3.2.7
|
'@vitest/expect': 3.2.7
|
||||||
@@ -8382,11 +8377,11 @@ snapshots:
|
|||||||
tinypool: 1.1.1
|
tinypool: 1.1.1
|
||||||
tinyrainbow: 2.0.0
|
tinyrainbow: 2.0.0
|
||||||
vite: 6.4.4(@types/node@26.6.5)(lightningcss@1.27.0)(terser@5.51.2)
|
vite: 6.4.4(@types/node@26.6.5)(lightningcss@1.27.0)(terser@5.51.2)
|
||||||
vite-node: 3.2.4(@types/node@26.6.5)(lightningcss@1.27.0)(terser@5.51.2)
|
vite-node: 3.2.4(@types/node@26.6.5)(lightningcss@1.27.0)(supports-color@10.2.2)(terser@5.51.2)
|
||||||
why-is-node-running: 2.3.0
|
why-is-node-running: 2.3.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@types/node': 26.6.5
|
'@types/node': 26.6.5
|
||||||
jsdom: 26.1.0
|
jsdom: 26.1.0(supports-color@10.2.2)
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- jiti
|
- jiti
|
||||||
- less
|
- less
|
||||||
|
|||||||
Reference in new issue
Block a user