From bdf1547a42360830c833bfadef3937de3897c1ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?= <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Sat, 18 Jul 2026 23:04:21 +0900 Subject: [PATCH 01/23] =?UTF-8?q?refactor(frontend):=20=E3=83=A1=E3=83=87?= =?UTF-8?q?=E3=82=A3=E3=82=A2=E3=83=A1=E3=83=8B=E3=83=A5=E3=83=BC=E3=81=AE?= =?UTF-8?q?=E3=83=A1=E3=83=8B=E3=83=A5=E3=83=BC=E3=83=9C=E3=82=BF=E3=83=B3?= =?UTF-8?q?=E3=81=AE=E3=82=B9=E3=82=BF=E3=82=A4=E3=83=AB=E3=82=92=E7=B5=B1?= =?UTF-8?q?=E4=B8=80=20(#17742)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix * fix * refactor --- .../frontend/src/components/MkMediaImage.vue | 32 +++++++------------ .../frontend/src/components/MkMediaVideo.vue | 24 +++++--------- 2 files changed, 20 insertions(+), 36 deletions(-) diff --git a/packages/frontend/src/components/MkMediaImage.vue b/packages/frontend/src/components/MkMediaImage.vue index 01f449753b..dee6db5e26 100644 --- a/packages/frontend/src/components/MkMediaImage.vue +++ b/packages/frontend/src/components/MkMediaImage.vue @@ -61,8 +61,8 @@ SPDX-License-Identifier: AGPL-3.0-only
ALT
- - + + @@ -174,23 +174,6 @@ function onContextmenu(ev: PointerEvent) { cursor: pointer; } -.hide { - display: block; - position: absolute; - background-color: rgba(0, 0, 0, 0.3); - -webkit-backdrop-filter: var(--MI-blur, blur(15px)); - backdrop-filter: var(--MI-blur, blur(15px)); - border-radius: 0 0 0 9px; - color: #fff; - font-size: 12px; - opacity: .5; - padding: 5px 8px; - text-align: center; - cursor: pointer; - top: 0; - right: 0; -} - .hiddenTextWrapper { display: table-cell; text-align: center; @@ -221,16 +204,25 @@ html[data-color-scheme=light] .visible { background-color: rgba(0, 0, 0, 0.3); -webkit-backdrop-filter: var(--MI-blur, blur(15px)); backdrop-filter: var(--MI-blur, blur(15px)); - border-radius: 9px 0 0 0; color: #fff; font-size: 0.8em; width: 28px; height: 28px; text-align: center; +} + +.menuBottom { + border-radius: 8px 0 8px 0; bottom: 0; right: 0; } +.menuTop { + border-radius: 0 8px 0 8px; + top: 0; + right: 0; +} + .imageContainer { display: block; overflow: hidden; diff --git a/packages/frontend/src/components/MkMediaVideo.vue b/packages/frontend/src/components/MkMediaVideo.vue index a03b58d479..d9d02b0afe 100644 --- a/packages/frontend/src/components/MkMediaVideo.vue +++ b/packages/frontend/src/components/MkMediaVideo.vue @@ -42,8 +42,8 @@ SPDX-License-Identifier: AGPL-3.0-only - - + + @@ -184,29 +184,21 @@ function onContextmenu(ev: PointerEvent) { background-color: rgba(0, 0, 0, 0.3); -webkit-backdrop-filter: var(--MI-blur, blur(15px)); backdrop-filter: var(--MI-blur, blur(15px)); - border-radius: 9px 0 0 0; color: #fff; font-size: 0.8em; width: 28px; height: 28px; text-align: center; +} + +.menuBottom { + border-radius: 8px 0 8px 0; bottom: 0; right: 0; } -.hide { - display: block; - position: absolute; - background-color: rgba(0, 0, 0, 0.3); - -webkit-backdrop-filter: var(--MI-blur, blur(15px)); - backdrop-filter: var(--MI-blur, blur(15px)); - border-radius: 0 0 0 9px; - color: #fff; - font-size: 12px; - opacity: .5; - padding: 5px 8px; - text-align: center; - cursor: pointer; +.menuTop { + border-radius: 0 8px 0 8px; top: 0; right: 0; } From 95de10ee653f2ea7b51ea5c365e1249322821ca1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?= <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Mon, 20 Jul 2026 08:19:18 +0900 Subject: [PATCH 02/23] fix(backend): fix dev mode --- packages/backend/rolldown.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/rolldown.config.ts b/packages/backend/rolldown.config.ts index e6a9888a09..768c3afc03 100644 --- a/packages/backend/rolldown.config.ts +++ b/packages/backend/rolldown.config.ts @@ -160,7 +160,7 @@ export default defineConfig((args) => { clearScreen: false, }, // ビルドの高速化のために、watchモードのときは外部モジュールは全てバンドルしないようにする - external: isWatchMode ? /^(?!@\/)[^.\/](?!:[\/\\])/ : externalModules, + external: isWatchMode ? /^(?!@\/|\0)[^.\/](?!:[\/\\])/ : externalModules, }; } }); From 9eca9b6f0d719c5cec9aa4294985ae63dffbda2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?= <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Mon, 20 Jul 2026 17:58:07 +0900 Subject: [PATCH 03/23] =?UTF-8?q?fix(frontend):=20MkLightbox=E3=82=92?= =?UTF-8?q?=E4=B8=8A=E4=B8=8B=E6=96=B9=E5=90=91=E3=81=AB=E3=82=B9=E3=83=AF?= =?UTF-8?q?=E3=82=A4=E3=83=97=E3=81=97=E3=81=A6=E9=96=89=E3=81=98=E3=82=8B?= =?UTF-8?q?=E3=81=93=E3=81=A8=E3=81=8C=E3=81=A7=E3=81=8D=E3=81=AA=E3=81=84?= =?UTF-8?q?=E3=81=93=E3=81=A8=E3=81=8C=E3=81=82=E3=82=8B=E5=95=8F=E9=A1=8C?= =?UTF-8?q?=E3=82=92=E4=BF=AE=E6=AD=A3=20(#17740)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(frontend): MkLightboxを上下方向にスワイプして閉じることができないことがある問題を修正 * fix * fix * fix * fix * fix * fix * fix * review fixes --- .../src/components/MkLightbox.item.vue | 127 ++++++++++++++---- .../frontend/src/components/MkLightbox.vue | 12 +- .../frontend/src/components/MkMediaImage.vue | 6 +- .../frontend/src/components/MkMediaVideo.vue | 7 +- 4 files changed, 115 insertions(+), 37 deletions(-) diff --git a/packages/frontend/src/components/MkLightbox.item.vue b/packages/frontend/src/components/MkLightbox.item.vue index eb7f581019..5870652d25 100644 --- a/packages/frontend/src/components/MkLightbox.item.vue +++ b/packages/frontend/src/components/MkLightbox.item.vue @@ -127,7 +127,6 @@ SPDX-License-Identifier: AGPL-3.0-only '; + + const html = renderHtml(base, head); + + expect(html).not.toContain(''); + expect(html).toContain('<script>alert(1)</script>'); +}); diff --git a/packages-private/diagnostics-frontend-browser/tsconfig.json b/packages-private/diagnostics-frontend-browser/tsconfig.json new file mode 100644 index 0000000000..6672c40acb --- /dev/null +++ b/packages-private/diagnostics-frontend-browser/tsconfig.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "strictFunctionTypes": true, + "strictNullChecks": true, + "esModuleInterop": true, + "skipLibCheck": true, + "noEmit": true, + "types": ["node"] + }, + "include": [ + "src/**/*.ts", + "test/**/*.ts" + ], + "exclude": [] +} diff --git a/packages-private/diagnostics-frontend-bundle/eslint.config.js b/packages-private/diagnostics-frontend-bundle/eslint.config.js new file mode 100644 index 0000000000..11bc648243 --- /dev/null +++ b/packages-private/diagnostics-frontend-bundle/eslint.config.js @@ -0,0 +1,25 @@ +import tsParser from '@typescript-eslint/parser'; +import sharedConfig from '../../packages/shared/eslint.config.js'; + +// eslint-disable-next-line import/no-default-export +export default [ + ...sharedConfig, + { + ignores: [ + '**/node_modules', + '**/__snapshots__', + 'test/fixtures', + ], + }, + { + files: ['**/*.ts', '**/*.tsx'], + languageOptions: { + parserOptions: { + parser: tsParser, + project: ['./tsconfig.json'], + sourceType: 'module', + tsconfigRootDir: import.meta.dirname, + }, + }, + }, +]; diff --git a/packages-private/diagnostics-frontend-bundle/package.json b/packages-private/diagnostics-frontend-bundle/package.json new file mode 100644 index 0000000000..b0271a8101 --- /dev/null +++ b/packages-private/diagnostics-frontend-bundle/package.json @@ -0,0 +1,22 @@ +{ + "name": "diagnostics-frontend-bundle", + "private": true, + "type": "module", + "scripts": { + "eslint": "eslint './**/*.{js,jsx,ts,tsx}'", + "lint": "pnpm typecheck && pnpm eslint", + "render-md": "tsx src/render-md.ts", + "test": "vitest run", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "diagnostics-shared": "workspace:*" + }, + "devDependencies": { + "@types/node": "26.1.1", + "tsx": "4.23.1", + "typescript": "5.9.3", + "vite": "8.1.4", + "vitest": "4.1.10" + } +} diff --git a/packages-private/diagnostics-frontend-bundle/src/chunk-report.ts b/packages-private/diagnostics-frontend-bundle/src/chunk-report.ts new file mode 100644 index 0000000000..2b2492377c --- /dev/null +++ b/packages-private/diagnostics-frontend-bundle/src/chunk-report.ts @@ -0,0 +1,217 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { + calcAndFormatDeltaBytes, + calcAndFormatDeltaPercentInMdTable, + escapeMdTableCell, + formatBytes, +} from 'diagnostics-shared/format'; +import type { CollectedReport, FileEntry } from './manifest'; + +/** + * この差分以下のチャンクは個別に出さず `(other)` にまとめる。 + * ハッシュ文字列の揺れ等でサイズが数バイト動くだけの行がノイズになるため。 + */ +const smallDeltaThreshold = 5; + +/** diff表に個別行として出す上限。これを超えた分は `(other)` に集約する */ +const diffRowLimit = 30; + +function entryDisplayName(entry: FileEntry | undefined) { + if (entry == null) return ''; + return entry.displayName || entry.file; +} + +export function getChunkComparisonRows(keys: string[], before: Partial>, after: Partial>) { + return keys.map(key => { + const beforeEntry = before[key]; + const afterEntry = after[key]; + const beforeSize = beforeEntry?.size ?? 0; + const afterSize = afterEntry?.size ?? 0; + return { + key, + name: entryDisplayName(beforeEntry ?? afterEntry), + beforeFile: beforeEntry?.file, + afterFile: afterEntry?.file, + beforeSize, + afterSize, + changeType: beforeEntry == null ? 'added' : afterEntry == null ? 'removed' : beforeSize !== afterSize ? 'updated' : 'unchanged', + sortSize: Math.max(beforeSize, afterSize), + }; + }); +} + +export type ChunkComparisonRow = ReturnType[number]; + +export type ChunkAggregate = { + beforeSize: number; + afterSize: number; + beforeCount: number; + afterCount: number; +}; + +export function sumChunkSizes(chunks: FileEntry[]) { + return chunks.reduce((sum, chunk) => sum + chunk.size, 0); +} + +/** + * 比較キーを持たない (= before/after で対応付けできない) チャンクの合計。 + */ +export function generatedAggregate(before: FileEntry[], after: FileEntry[]): ChunkAggregate { + const beforeGenerated = before.filter(chunk => chunk.comparisonKey == null); + const afterGenerated = after.filter(chunk => chunk.comparisonKey == null); + return { + beforeSize: sumChunkSizes(beforeGenerated), + afterSize: sumChunkSizes(afterGenerated), + beforeCount: beforeGenerated.length, + afterCount: afterGenerated.length, + }; +} + +export function hasSmallDelta(row: ChunkComparisonRow) { + return Math.abs(row.afterSize - row.beforeSize) <= smallDeltaThreshold; +} + +export function comparisonRowsAggregate(rows: ChunkComparisonRow[]): ChunkAggregate { + return { + beforeSize: rows.reduce((sum, row) => sum + row.beforeSize, 0), + afterSize: rows.reduce((sum, row) => sum + row.afterSize, 0), + beforeCount: rows.filter(row => row.beforeFile != null).length, + afterCount: rows.filter(row => row.afterFile != null).length, + }; +} + +export function comparableMap(chunks: FileEntry[]) { + const entries: [string, FileEntry][] = []; + for (const chunk of chunks) { + if (chunk.comparisonKey != null) entries.push([chunk.comparisonKey, chunk]); + } + return Object.fromEntries(entries); +} + +export function summarizeChunkChanges(rows: ChunkComparisonRow[]) { + return { + updated: rows.filter((row) => row.changeType === 'updated').length, + added: rows.filter((row) => row.changeType === 'added').length, + removed: rows.filter((row) => row.changeType === 'removed').length, + }; +} + +export function formatChunkChangeSummary(label: string, summary: ReturnType) { + return `${label} (${summary.updated} updated, ${summary.added} added, ${summary.removed} removed)`; +} + +/** + * 差分の絶対値が大きい順。同着は増加側・元サイズ・名前の順で決定的に並べる。 + */ +export function compareChunkComparisonRows(a: ChunkComparisonRow, b: ChunkComparisonRow) { + return Math.abs(b.afterSize - b.beforeSize) - Math.abs(a.afterSize - a.beforeSize) + || (b.afterSize - b.beforeSize) - (a.afterSize - a.beforeSize) + || b.sortSize - a.sortSize + || a.name.localeCompare(b.name); +} + +export function chunkFileDisplay(row: ChunkComparisonRow) { + if (row.beforeFile == null) return row.afterFile ?? ''; + if (row.afterFile == null || row.beforeFile === row.afterFile) return row.beforeFile; + return `${row.beforeFile} → ${row.afterFile}`; +} + +export function chunkMarkdownTable( + rows: ChunkComparisonRow[], + total?: { beforeSize: number; afterSize: number }, + generated?: ChunkAggregate, + other?: ChunkAggregate, +) { + const hasGenerated = generated != null && (generated.beforeCount > 0 || generated.afterCount > 0); + const hasOther = other != null && (other.beforeCount > 0 || other.afterCount > 0); + if (rows.length === 0 && total == null && !hasGenerated && !hasOther) return '_No data_'; + + const lines = [ + '| Chunk | Before | After | Δ | Δ (%) |', + '| --- | ---: | ---: | ---: | ---: |', + ]; + if (total != null) { + lines.push(`| (total) | ${formatBytes(total.beforeSize)} | ${formatBytes(total.afterSize)} | ${calcAndFormatDeltaBytes(total.beforeSize, total.afterSize, 1000)} | ${calcAndFormatDeltaPercentInMdTable(total.beforeSize, total.afterSize, 0.1)} |`); + lines.push('| | | | | |'); + } + + for (const row of rows) { + const chunkFile = chunkFileDisplay(row); + if (row.changeType === 'added') { + lines.push(`|
\`${escapeMdTableCell(row.name)}\` \`${escapeMdTableCell(chunkFile)}\`
| ${formatBytes(row.beforeSize)} | ${formatBytes(row.afterSize)} | ${calcAndFormatDeltaBytes(row.beforeSize, row.afterSize, 1000)} | $\\color{orange}{\\text{( + )}}$ |`); + } else if (row.changeType === 'removed') { + lines.push(`|
\`${escapeMdTableCell(row.name)}\` \`${escapeMdTableCell(chunkFile)}\`
| ${formatBytes(row.beforeSize)} | ${formatBytes(row.afterSize)} | ${calcAndFormatDeltaBytes(row.beforeSize, row.afterSize, 1000)} | $\\color{green}{\\text{( - )}}$ |`); + } else { + lines.push(`|
\`${escapeMdTableCell(row.name)}\` \`${escapeMdTableCell(chunkFile)}\`
| ${formatBytes(row.beforeSize)} | ${formatBytes(row.afterSize)} | ${calcAndFormatDeltaBytes(row.beforeSize, row.afterSize, 1000)} | ${calcAndFormatDeltaPercentInMdTable(row.beforeSize, row.afterSize, 0.1)} |`); + } + } + if (hasGenerated) { + lines.push(`| (other generated chunks) | ${formatBytes(generated.beforeSize)} | ${formatBytes(generated.afterSize)} | ${calcAndFormatDeltaBytes(generated.beforeSize, generated.afterSize, 1000)} | ${calcAndFormatDeltaPercentInMdTable(generated.beforeSize, generated.afterSize, 0.1)} |`); + } + if (hasOther) { + lines.push(`| (other) | ${formatBytes(other.beforeSize)} | ${formatBytes(other.afterSize)} | ${calcAndFormatDeltaBytes(other.beforeSize, other.afterSize, 1000)} | ${calcAndFormatDeltaPercentInMdTable(other.beforeSize, other.afterSize, 0.1)} |`); + } + return lines.join('\n'); +} + +export function renderFrontendChunkReport(before: CollectedReport, after: CollectedReport) { + const beforeComparable = before.comparableChunks; + const afterComparable = after.comparableChunks; + const allChunkKeys = [...new Set([...Object.keys(beforeComparable), ...Object.keys(afterComparable)])]; + const allComparisonRows = getChunkComparisonRows(allChunkKeys, beforeComparable, afterComparable); + + const changedRows = allComparisonRows.filter((row) => row.changeType !== 'unchanged'); + const diffSummary = summarizeChunkChanges(changedRows); + const diffTotal = { + beforeSize: sumChunkSizes(before.chunks), + afterSize: sumChunkSizes(after.chunks), + }; + const diffGenerated = generatedAggregate(before.chunks, after.chunks); + const largeDeltaRows = changedRows.filter(row => !hasSmallDelta(row)).sort(compareChunkComparisonRows); + const diffRows = largeDeltaRows.slice(0, diffRowLimit); + // 表示上限で切り捨てた行も `(other)` に含める。落とすと合計が実際の変化量と合わなくなる + const diffOther = comparisonRowsAggregate([ + ...changedRows.filter(hasSmallDelta), + ...largeDeltaRows.slice(diffRowLimit), + ]); + + const beforeStartupFiles = new Set(before.startupFiles); + const afterStartupFiles = new Set(after.startupFiles); + const beforeStartupChunks = before.chunks.filter(chunk => beforeStartupFiles.has(chunk.file)); + const afterStartupChunks = after.chunks.filter(chunk => afterStartupFiles.has(chunk.file)); + const beforeStartupComparable = comparableMap(beforeStartupChunks); + const afterStartupComparable = comparableMap(afterStartupChunks); + const startupKeys = [...new Set([...Object.keys(beforeStartupComparable), ...Object.keys(afterStartupComparable)])]; + const startupComparisonRows = getChunkComparisonRows(startupKeys, beforeStartupComparable, afterStartupComparable); + const startupSummary = summarizeChunkChanges(startupComparisonRows); + const startupOther = comparisonRowsAggregate(startupComparisonRows.filter(hasSmallDelta)); + const startupRows = startupComparisonRows.filter(row => !hasSmallDelta(row)).sort(compareChunkComparisonRows); + const startupTotal = { + beforeSize: sumChunkSizes(beforeStartupChunks), + afterSize: sumChunkSizes(afterStartupChunks), + }; + const startupGenerated = generatedAggregate(beforeStartupChunks, afterStartupChunks); + + return [ + '
', + `${formatChunkChangeSummary('Chunk size diff', diffSummary)}`, + '', + chunkMarkdownTable(diffRows, diffTotal, diffGenerated, diffOther), + '', + '
', + '', + '
', + `${formatChunkChangeSummary('Startup chunk size', startupSummary)}`, + '', + chunkMarkdownTable(startupRows, startupTotal, startupGenerated, startupOther), + '', + '_Startup chunks are the Vite entry for \`src/_boot_.ts\` and its static imports._', + '', + '
', + '', + ].join('\n'); +} diff --git a/packages-private/diagnostics-frontend-bundle/src/fs-utils.ts b/packages-private/diagnostics-frontend-bundle/src/fs-utils.ts new file mode 100644 index 0000000000..74ed757b53 --- /dev/null +++ b/packages-private/diagnostics-frontend-bundle/src/fs-utils.ts @@ -0,0 +1,40 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { promises as fs } from 'node:fs'; +import path from 'node:path'; + +/** + * Windows の `\` 区切りを `/` に揃える。manifest 側のキーが常に `/` 区切りのため、 + * 実ファイルパスと突き合わせる前に正規化する必要がある。 + */ +export function normalizePath(filePath: string) { + return filePath.split(path.sep).join('/'); +} + +export async function fileExists(filePath: string) { + try { + await fs.access(filePath); + return true; + } catch { + return false; + } +} + +export async function fileSize(filePath: string) { + const stat = await fs.stat(filePath); + return stat.size; +} + +export async function* traverseDirectory(dir: string): AsyncGenerator { + for (const entry of await fs.readdir(dir, { withFileTypes: true })) { + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + yield* traverseDirectory(fullPath); + } else if (entry.isFile()) { + yield fullPath; + } + } +} diff --git a/packages-private/diagnostics-frontend-bundle/src/manifest.ts b/packages-private/diagnostics-frontend-bundle/src/manifest.ts new file mode 100644 index 0000000000..d35a5cfbfb --- /dev/null +++ b/packages-private/diagnostics-frontend-bundle/src/manifest.ts @@ -0,0 +1,171 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { promises as fs } from 'node:fs'; +import path from 'node:path'; +import { fileExists, fileSize, normalizePath, traverseDirectory } from './fs-utils'; +import type { Manifest, ManifestChunk } from 'vite'; + +/** + * 比較対象とするロケール。ロケール別チャンクは全ロケール分だと数が多すぎるため、 + * 代表として ja-JP のみを見る。 + */ +const locale = 'ja-JP'; + +/** + * `src` を持たないチャンクのうち、名前がビルド間で安定していて比較可能なもの。 + */ +const stableNamedChunks = new Set(['vue', 'i18n']); + +export type FileEntry = { + comparisonKey: string | null; + displayName: string; + file: string; + manifestKeys: string[]; + size: number; +}; + +export type CollectedReport = { + manifest: Manifest; + chunks: FileEntry[]; + comparableChunks: Record; + chunksByManifestKey: Record; + startupFiles: string[]; +}; + +export function findEntryKey(manifest: Manifest) { + const entries = Object.entries(manifest); + return entries.find(([key, chunk]) => key === 'src/_boot_.ts' || chunk.src === 'src/_boot_.ts')?.[0] + ?? entries.find(([, chunk]) => chunk.name === 'entry' && chunk.isEntry)?.[0] + ?? entries.find(([, chunk]) => chunk.isEntry)?.[0] + ?? null; +} + +/** + * ビルド間で安定するチャンク識別子。出力ファイル名はハッシュ付きで毎回変わるため、 + * これが取れないチャンクは before/after の対応付けができない。 + */ +export function stableChunkKey(chunk: ManifestChunk) { + if (chunk.src != null) return `src:${normalizePath(chunk.src)}`; + if (chunk.name != null && stableNamedChunks.has(chunk.name)) return `named:${chunk.name}`; + return null; +} + +/** + * 起動時に必ず読み込まれるチャンク (entry とその静的 import) の manifest キーを集める。 + */ +export function collectStartupManifestKeys(manifest: Manifest) { + const entryKey = findEntryKey(manifest); + const keys = new Set(); + if (entryKey == null) throw new Error('Unable to find frontend startup entry in Vite manifest.'); + + function visit(key: string, importedBy?: string) { + if (keys.has(key)) return; + const chunk = manifest[key]; + const importContext = importedBy == null ? '' : ` imported by "${importedBy}"`; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (chunk == null) throw new Error(`Startup manifest key "${key}"${importContext} is missing.`); + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (chunk.file == null || chunk.file.length === 0) throw new Error(`Startup manifest key "${key}"${importContext} has no output file.`); + if (!chunk.file.endsWith('.js')) throw new Error(`Startup manifest key "${key}"${importContext} resolves to non-JavaScript output "${chunk.file}".`); + keys.add(key); + for (const importKey of chunk.imports ?? []) visit(importKey, key); + } + + visit(entryKey); + return keys; +} + +/** + * manifest 上の出力パスを実ファイルへ解決する。`scripts/` 配下はロケール別に + * 複製されて出力されるため、代表ロケールのものへ読み替える。 + */ +export async function resolveBuiltFile(outDir: string, file: string) { + if (file.startsWith('scripts/')) { + const localizedFile = file.slice('scripts/'.length); + const localizedPath = path.join(outDir, locale, localizedFile); + if (await fileExists(localizedPath)) { + return { + absolutePath: localizedPath, + relativePath: `${locale}/${localizedFile}`, + }; + } + + throw new Error(`Expected ${locale} localized chunk for ${file}, but ${localizedPath} was not found.`); + } + return { + absolutePath: path.join(outDir, file), + relativePath: file, + }; +} + +export async function collectReport(repoDir: string): Promise { + const outDir = path.join(repoDir, 'built/_frontend_vite_'); + const manifestPath = path.join(outDir, 'manifest.json'); + const manifest = JSON.parse(await fs.readFile(manifestPath, 'utf8')) as Manifest; + const chunksByFile = new Map(); + const comparableChunks = new Map(); + const chunksByManifestKey = new Map(); + + for (const [manifestKey, chunk] of Object.entries(manifest)) { + if (!chunk.file.endsWith('.js')) continue; + const builtFile = await resolveBuiltFile(outDir, chunk.file); + const comparisonKey = stableChunkKey(chunk); + let entry = chunksByFile.get(builtFile.relativePath); + if (entry == null) { + entry = { + comparisonKey, + displayName: chunk.src ?? chunk.name ?? manifestKey, + file: builtFile.relativePath, + manifestKeys: [manifestKey], + size: await fileSize(builtFile.absolutePath), + }; + chunksByFile.set(entry.file, entry); + } else if (entry.comparisonKey !== comparisonKey) { + throw new Error(`Conflicting identities for ${entry.file}`); + } else { + entry.manifestKeys.push(manifestKey); + } + chunksByManifestKey.set(manifestKey, entry); + if (comparisonKey != null) { + const existing = comparableChunks.get(comparisonKey); + if (existing != null && existing.file !== entry.file) { + throw new Error(`Duplicate stable chunk key "${comparisonKey}": ${existing.file}, ${entry.file}`); + } + comparableChunks.set(comparisonKey, entry); + } + } + + // manifest に載らないロケール別チャンクも合計サイズには含めたいので拾っておく + const localeDir = path.join(outDir, locale); + if (await fileExists(localeDir)) { + for await (const fullPath of traverseDirectory(localeDir)) { + if (!fullPath.endsWith('.js')) continue; + const relativePath = normalizePath(path.relative(outDir, fullPath)); + if (chunksByFile.has(relativePath)) continue; + chunksByFile.set(relativePath, { + comparisonKey: null, + displayName: relativePath, + file: relativePath, + manifestKeys: [], + size: await fileSize(fullPath), + }); + } + } + + const startupFiles = new Set(); + for (const manifestKey of collectStartupManifestKeys(manifest)) { + const entry = chunksByManifestKey.get(manifestKey); + if (entry != null) startupFiles.add(entry.file); + } + + return { + manifest, + chunks: [...chunksByFile.values()], + comparableChunks: Object.fromEntries(comparableChunks), + chunksByManifestKey: Object.fromEntries(chunksByManifestKey), + startupFiles: [...startupFiles], + }; +} diff --git a/packages-private/diagnostics-frontend-bundle/src/render-md.ts b/packages-private/diagnostics-frontend-bundle/src/render-md.ts new file mode 100644 index 0000000000..d8fe0997dd --- /dev/null +++ b/packages-private/diagnostics-frontend-bundle/src/render-md.ts @@ -0,0 +1,32 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { promises as fs } from 'node:fs'; +import path from 'node:path'; +import { readRequiredEnv } from 'diagnostics-shared/env'; +import { collectReport } from './manifest'; +import { renderBundleReportMarkdown } from './report'; +import type { VisualizerReport } from './visualizer'; + +async function main() { + const [beforeDir, afterDir, beforeStatsFile, afterStatsFile, outFile] = process.argv.slice(2).map(arg => path.resolve(arg)); + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (outFile == null) throw new Error('Usage: render-md '); + + // 未設定のまま `undefined` という文字列をコメントに埋め込まないよう、ここで落とす + const visualizerArtifactUrl = readRequiredEnv('FRONTEND_BUNDLE_REPORT_ARTIFACT_URL'); + + const before = await collectReport(beforeDir); + const after = await collectReport(afterDir); + const beforeStats = JSON.parse(await fs.readFile(beforeStatsFile, 'utf8')) as VisualizerReport; + const afterStats = JSON.parse(await fs.readFile(afterStatsFile, 'utf8')) as VisualizerReport; + + await fs.writeFile(outFile, renderBundleReportMarkdown(before, after, beforeStats, afterStats, { visualizerArtifactUrl })); +} + +await main().catch(err => { + console.error(err); + process.exit(1); +}); diff --git a/packages-private/diagnostics-frontend-bundle/src/report.ts b/packages-private/diagnostics-frontend-bundle/src/report.ts new file mode 100644 index 0000000000..db2e1cf6e3 --- /dev/null +++ b/packages-private/diagnostics-frontend-bundle/src/report.ts @@ -0,0 +1,33 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { renderFrontendChunkReport } from './chunk-report'; +import { collectVisualizerReport, renderVisualizerSummaryTable, type VisualizerReport } from './visualizer'; +import type { CollectedReport } from './manifest'; + +export type RenderBundleReportOptions = { + /** rollup-plugin-visualizer が出力したtreemap HTMLのartifact URL */ + visualizerArtifactUrl: string; +}; + +export function renderBundleReportMarkdown( + before: CollectedReport, + after: CollectedReport, + beforeStats: VisualizerReport, + afterStats: VisualizerReport, + options: RenderBundleReportOptions, +) { + return [ + '## 📦 Frontend Bundle Report', + '', + renderFrontendChunkReport(before, after), + '', + '## Bundle Stats', + '', + renderVisualizerSummaryTable(collectVisualizerReport(beforeStats), collectVisualizerReport(afterStats)), + '', + `[Open treemap HTML](${options.visualizerArtifactUrl})`, + ].join('\n'); +} diff --git a/packages-private/diagnostics-frontend-bundle/src/visualizer.ts b/packages-private/diagnostics-frontend-bundle/src/visualizer.ts new file mode 100644 index 0000000000..17e4f190b6 --- /dev/null +++ b/packages-private/diagnostics-frontend-bundle/src/visualizer.ts @@ -0,0 +1,204 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { + calcAndFormatDeltaBytes, + calcAndFormatDeltaNumber, + calcAndFormatDeltaPercent, + formatBytes, + formatNumber, +} from 'diagnostics-shared/format'; + +/** + * rollup-plugin-visualizer が出力する `stats.json` のうち、ここで使う部分のみの型。 + */ +export type VisualizerReport = { + nodeParts?: Record; + nodeMetas?: Record; + renderedLength: number; + gzipLength: number; + brotliLength: number; + }>; + options?: Record; +}; + +type ModuleRow = { + id: string; + bundles: number; + renderedLength: number; + gzipLength: number; + brotliLength: number; + importedByCount: number; + importedCount: number; +}; + +type BundleRow = { + id: string; + modules: number; + renderedLength: number; + gzipLength: number; + brotliLength: number; +}; + +export function collectVisualizerReport(data: VisualizerReport) { + const nodeParts = data.nodeParts ?? {}; + const nodeMetas = Object.values(data.nodeMetas ?? {}); + const moduleRows: ModuleRow[] = []; + const bundleMap = new Map(); + + for (const meta of nodeMetas) { + const row: ModuleRow = { + id: meta.id, + bundles: 0, + renderedLength: 0, + gzipLength: 0, + brotliLength: 0, + importedByCount: meta.importedBy?.length ?? 0, + importedCount: meta.imported?.length ?? 0, + }; + + for (const [bundleId, partUid] of Object.entries(meta.moduleParts ?? {})) { + const part = nodeParts[partUid]; + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (part == null) continue; + + row.bundles += 1; + row.renderedLength += part.renderedLength; + row.gzipLength += part.gzipLength; + row.brotliLength += part.brotliLength; + + const bundle = bundleMap.get(bundleId) ?? { + id: bundleId, + modules: 0, + renderedLength: 0, + gzipLength: 0, + brotliLength: 0, + }; + bundle.modules += 1; + bundle.renderedLength += part.renderedLength; + bundle.gzipLength += part.gzipLength; + bundle.brotliLength += part.brotliLength; + bundleMap.set(bundleId, bundle); + } + + // どのバンドルにも含まれないモジュール (tree-shake 済み等) は集計対象外 + if (row.bundles > 0) { + moduleRows.push(row); + } + } + + let staticImports = 0; + let dynamicImports = 0; + for (const meta of nodeMetas) { + for (const imported of meta.imported ?? []) { + if (imported.dynamic) { + dynamicImports += 1; + } else { + staticImports += 1; + } + } + } + + const bundleRows = [...bundleMap.values()].sort((a, b) => b.renderedLength - a.renderedLength); + const hotModules = [...moduleRows].sort((a, b) => b.renderedLength - a.renderedLength); + const totalRendered = moduleRows.reduce((sum, row) => sum + row.renderedLength, 0); + const totalGzip = moduleRows.reduce((sum, row) => sum + row.gzipLength, 0); + const totalBrotli = moduleRows.reduce((sum, row) => sum + row.brotliLength, 0); + + return { + options: data.options ?? {}, + summary: { + bundles: bundleRows.length, + modules: moduleRows.length, + entries: nodeMetas.filter((meta) => meta.isEntry).length, + externals: nodeMetas.filter((meta) => meta.isExternal).length, + staticImports, + dynamicImports, + }, + metrics: { + renderedLength: totalRendered, + gzipLength: totalGzip, + brotliLength: totalBrotli, + }, + hotModules, + }; +} + +/** + * NOTE: 以前はこの関数が `string[]` を返しており、呼び出し側の `[...].join('\n')` の + * 要素として配列のまま埋め込まれていたため、テーブルがカンマ区切りの1行に潰れていた。 + * 呼び出し側で意識しなくて済むよう、ここで文字列にして返す。 + */ +export function renderVisualizerSummaryTable(before: ReturnType, after: ReturnType) { + const summary = [ + 'bundles', + 'modules', + 'entries', + //'externals', + 'staticImports', + 'dynamicImports', + ] as const; + + const metrics = [ + 'renderedLength', + 'gzipLength', + 'brotliLength', + ] as const; + + return [ + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + '', + ...summary.map((key) => ``), + ...metrics.map((key) => ``), + '', + '', + '', + ...summary.map((key) => ``), + ...metrics.map((key) => ``), + '', + '', + '', + '', + ...summary.map((key) => ``), + ...metrics.map((key) => ``), + '', + '', + '', + ...summary.map((key) => ``), + ...metrics.map((key) => ``), + '', + '', + '
BundlesModulesEntriesImportsSize
StaticDynamicRenderedGzipBrotli
Before${formatNumber(before.summary[key])}${formatBytes(before.metrics[key])}
After${formatNumber(after.summary[key])}${formatBytes(after.metrics[key])}
Δ${calcAndFormatDeltaNumber(before.summary[key], after.summary[key], 0)}${calcAndFormatDeltaBytes(before.metrics[key], after.metrics[key], 1000)}
Δ (%)${calcAndFormatDeltaPercent(before.summary[key], after.summary[key], 0.1)}${calcAndFormatDeltaPercent(before.metrics[key], after.metrics[key], 0.1)}
', + ].join('\n'); +} diff --git a/packages-private/diagnostics-frontend-bundle/test/__snapshots__/render-md.md b/packages-private/diagnostics-frontend-bundle/test/__snapshots__/render-md.md new file mode 100644 index 0000000000..867cbf3b75 --- /dev/null +++ b/packages-private/diagnostics-frontend-bundle/test/__snapshots__/render-md.md @@ -0,0 +1,100 @@ +## 📦 Frontend Bundle Report + +
+Chunk size diff (2 updated, 0 added, 0 removed) + +| Chunk | Before | After | Δ | Δ (%) | +| --- | ---: | ---: | ---: | ---: | +| (total) | 120 KB | 127 KB | $\color{orange}{\text{+6.3 KB}}$ | $\color{orange}{\text{+5.2\\%}}$ | +| | | | | | +|
`vue` `assets/vue-b2.js`
| 90 KB | 96 KB | $\color{orange}{\text{+6 KB}}$ | $\color{orange}{\text{+6.7\\%}}$ | +| (other generated chunks) | 1.2 KB | 1.5 KB | $\text{+300 B}$ | $\color{orange}{\text{+25\\%}}$ | +| (other) | 20 KB | 20 KB | $\text{+3 B}$ | $\text{+0\\%}$ | + +
+ +
+Startup chunk size (2 updated, 0 added, 0 removed) + +| Chunk | Before | After | Δ | Δ (%) | +| --- | ---: | ---: | ---: | ---: | +| (total) | 114 KB | 120 KB | $\color{orange}{\text{+6 KB}}$ | $\color{orange}{\text{+5.3\\%}}$ | +| | | | | | +|
`vue` `assets/vue-b2.js`
| 90 KB | 96 KB | $\color{orange}{\text{+6 KB}}$ | $\color{orange}{\text{+6.7\\%}}$ | +| (other) | 24 KB | 24 KB | $\text{+3 B}$ | $\text{+0\\%}$ | + +_Startup chunks are the Vite entry for `src/_boot_.ts` and its static imports._ + +
+ + +## Bundle Stats + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
BundlesModulesEntriesImportsSize
StaticDynamicRenderedGzipBrotli
Before2612321 KB6.3 KB5.3 KB
After2713331 KB8.4 KB7 KB
Δ0$\color{orange}{\text{+1}}$0$\color{orange}{\text{+1}}$0$\color{orange}{\text{+9.8 KB}}$$\color{orange}{\text{+2.1 KB}}$$\color{orange}{\text{+1.8 KB}}$
Δ (%)0%$\color{orange}{\text{+16.7\%}}$0%$\color{orange}{\text{+50\%}}$0%$\color{orange}{\text{+46.7\%}}$$\color{orange}{\text{+33.3\%}}$$\color{orange}{\text{+33.3\%}}$
+ +[Open treemap HTML](https://example.invalid/treemap) \ No newline at end of file diff --git a/packages-private/diagnostics-frontend-bundle/test/fixtures/after-stats.json b/packages-private/diagnostics-frontend-bundle/test/fixtures/after-stats.json new file mode 100644 index 0000000000..b69e3f4cea --- /dev/null +++ b/packages-private/diagnostics-frontend-bundle/test/fixtures/after-stats.json @@ -0,0 +1 @@ +{"nodeParts": {"p0": {"renderedLength": 1100.0, "gzipLength": 300, "brotliLength": 250}, "p1": {"renderedLength": 2200.0, "gzipLength": 600, "brotliLength": 500}, "p2": {"renderedLength": 3300.0000000000005, "gzipLength": 900, "brotliLength": 750}, "p3": {"renderedLength": 4400.0, "gzipLength": 1200, "brotliLength": 1000}, "p4": {"renderedLength": 5500.0, "gzipLength": 1500, "brotliLength": 1250}, "p5": {"renderedLength": 6600.000000000001, "gzipLength": 1800, "brotliLength": 1500}, "p6": {"renderedLength": 7700.000000000001, "gzipLength": 2100, "brotliLength": 1750}}, "nodeMetas": {"m0": {"id": "/src/mod0.ts", "isEntry": true, "importedBy": [], "imported": [{"id": "m1", "dynamic": true}], "moduleParts": {"assets/boot-a1.js": "p0"}, "renderedLength": 0, "gzipLength": 0, "brotliLength": 0}, "m1": {"id": "/src/mod1.ts", "isEntry": false, "importedBy": ["m0"], "imported": [{"id": "m2", "dynamic": false}], "moduleParts": {"assets/vue-b2.js": "p1"}, "renderedLength": 0, "gzipLength": 0, "brotliLength": 0}, "m2": {"id": "/src/mod2.ts", "isEntry": false, "importedBy": ["m1"], "imported": [{"id": "m3", "dynamic": true}], "moduleParts": {"assets/boot-a1.js": "p2"}, "renderedLength": 0, "gzipLength": 0, "brotliLength": 0}, "m3": {"id": "/src/mod3.ts", "isEntry": false, "importedBy": ["m2"], "imported": [{"id": "m4", "dynamic": false}], "moduleParts": {"assets/vue-b2.js": "p3"}, "renderedLength": 0, "gzipLength": 0, "brotliLength": 0}, "m4": {"id": "/src/mod4.ts", "isEntry": false, "importedBy": ["m3"], "imported": [{"id": "m5", "dynamic": true}], "moduleParts": {"assets/boot-a1.js": "p4"}, "renderedLength": 0, "gzipLength": 0, "brotliLength": 0}, "m5": {"id": "/src/mod5.ts", "isEntry": false, "importedBy": ["m4"], "imported": [{"id": "m6", "dynamic": false}], "moduleParts": {"assets/vue-b2.js": "p5"}, "renderedLength": 0, "gzipLength": 0, "brotliLength": 0}, "m6": {"id": "/src/mod6.ts", "isEntry": false, "importedBy": ["m5"], "imported": [], "moduleParts": {"assets/boot-a1.js": "p6"}, "renderedLength": 0, "gzipLength": 0, "brotliLength": 0}}, "options": {}} \ No newline at end of file diff --git a/packages-private/diagnostics-frontend-bundle/test/fixtures/before-stats.json b/packages-private/diagnostics-frontend-bundle/test/fixtures/before-stats.json new file mode 100644 index 0000000000..0a54b93148 --- /dev/null +++ b/packages-private/diagnostics-frontend-bundle/test/fixtures/before-stats.json @@ -0,0 +1 @@ +{"nodeParts": {"p0": {"renderedLength": 1000, "gzipLength": 300, "brotliLength": 250}, "p1": {"renderedLength": 2000, "gzipLength": 600, "brotliLength": 500}, "p2": {"renderedLength": 3000, "gzipLength": 900, "brotliLength": 750}, "p3": {"renderedLength": 4000, "gzipLength": 1200, "brotliLength": 1000}, "p4": {"renderedLength": 5000, "gzipLength": 1500, "brotliLength": 1250}, "p5": {"renderedLength": 6000, "gzipLength": 1800, "brotliLength": 1500}}, "nodeMetas": {"m0": {"id": "/src/mod0.ts", "isEntry": true, "importedBy": [], "imported": [{"id": "m1", "dynamic": true}], "moduleParts": {"assets/boot-a1.js": "p0"}, "renderedLength": 0, "gzipLength": 0, "brotliLength": 0}, "m1": {"id": "/src/mod1.ts", "isEntry": false, "importedBy": ["m0"], "imported": [{"id": "m2", "dynamic": false}], "moduleParts": {"assets/vue-b2.js": "p1"}, "renderedLength": 0, "gzipLength": 0, "brotliLength": 0}, "m2": {"id": "/src/mod2.ts", "isEntry": false, "importedBy": ["m1"], "imported": [{"id": "m3", "dynamic": true}], "moduleParts": {"assets/boot-a1.js": "p2"}, "renderedLength": 0, "gzipLength": 0, "brotliLength": 0}, "m3": {"id": "/src/mod3.ts", "isEntry": false, "importedBy": ["m2"], "imported": [{"id": "m4", "dynamic": false}], "moduleParts": {"assets/vue-b2.js": "p3"}, "renderedLength": 0, "gzipLength": 0, "brotliLength": 0}, "m4": {"id": "/src/mod4.ts", "isEntry": false, "importedBy": ["m3"], "imported": [{"id": "m5", "dynamic": true}], "moduleParts": {"assets/boot-a1.js": "p4"}, "renderedLength": 0, "gzipLength": 0, "brotliLength": 0}, "m5": {"id": "/src/mod5.ts", "isEntry": false, "importedBy": ["m4"], "imported": [], "moduleParts": {"assets/vue-b2.js": "p5"}, "renderedLength": 0, "gzipLength": 0, "brotliLength": 0}}, "options": {}} \ No newline at end of file diff --git a/packages-private/diagnostics-frontend-bundle/test/render-md.test.ts b/packages-private/diagnostics-frontend-bundle/test/render-md.test.ts new file mode 100644 index 0000000000..17adcef883 --- /dev/null +++ b/packages-private/diagnostics-frontend-bundle/test/render-md.test.ts @@ -0,0 +1,105 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { dirname, join } from 'node:path'; +import { afterAll, beforeAll, expect, test } from 'vitest'; +import { collectReport } from '../src/manifest'; +import { renderBundleReportMarkdown } from '../src/report'; +import type { VisualizerReport } from '../src/visualizer'; + +const fixturesDir = join(import.meta.dirname, 'fixtures'); + +/** + * ビルド成果物のfixture。 + * + * `collectReport` はファイルの中身を見ずサイズしか使わないので、実体は指定バイト数の + * 詰め物でよい。ディレクトリ名が `built` になるためリポジトリにはコミットできず + * (ルートの .gitignore がビルド成果物として除外する)、テスト実行時に組み立てている。 + */ +const manifest = { + 'src/_boot_.ts': { file: 'assets/boot-a1.js', src: 'src/_boot_.ts', name: 'boot', isEntry: true, imports: ['_vue.js', '_i18n.js'] }, + '_vue.js': { file: 'assets/vue-b2.js', name: 'vue' }, + // `scripts/` 配下はロケール別に出力されるので ja-JP/ に解決される + '_i18n.js': { file: 'scripts/i18n-c3.js', name: 'i18n' }, + 'src/pages/foo.vue': { file: 'assets/foo-d4.js', src: 'src/pages/foo.vue', name: 'foo' }, + // .js 以外はチャンクとして数えない + 'src/pages/style.css': { file: 'assets/style-e5.css', src: 'src/pages/style.css' }, +}; + +const fileSizes = { + before: { + 'assets/boot-a1.js': 20_000, + 'assets/vue-b2.js': 90_000, + 'assets/foo-d4.js': 5_000, + 'assets/style-e5.css': 100, + 'ja-JP/i18n-c3.js': 4_000, + 'ja-JP/orphan.js': 1_200, + }, + after: { + // 差が小さすぎる (閾値5バイト以下) ので「(other)」に集約される + 'assets/boot-a1.js': 20_003, + // 明確に増えるので diff表に行として出る + 'assets/vue-b2.js': 96_000, + 'assets/foo-d4.js': 5_000, + 'assets/style-e5.css': 100, + 'ja-JP/i18n-c3.js': 4_000, + // manifestに載らない出力なので「(other generated chunks)」に集約される + 'ja-JP/orphan.js': 1_500, + }, +} as const satisfies Record<'before' | 'after', Record>; + +let repoDirs: { before: string; after: string }; +let workDir: string; + +beforeAll(async () => { + workDir = await mkdtemp(join(tmpdir(), 'diagnostics-frontend-bundle-')); + + for (const label of ['before', 'after'] as const) { + const outDir = join(workDir, label, 'built/_frontend_vite_'); + await mkdir(outDir, { recursive: true }); + await writeFile(join(outDir, 'manifest.json'), JSON.stringify(manifest)); + + for (const [file, size] of Object.entries(fileSizes[label])) { + const path = join(outDir, file); + await mkdir(dirname(path), { recursive: true }); + await writeFile(path, 'x'.repeat(size)); + } + } + + repoDirs = { + before: join(workDir, 'before'), + after: join(workDir, 'after'), + }; +}); + +afterAll(async () => { + await rm(workDir, { recursive: true, force: true }); +}); + +async function loadStats(name: string) { + return JSON.parse(await readFile(join(fixturesDir, `${name}-stats.json`), 'utf8')) as VisualizerReport; +} + +/** + * 出力をゴールデンファイルで固定する。 + * 意図的に変更したときは `vitest -u` で更新し、__snapshots__ の差分もレビューすること。 + */ +test('renders the frontend bundle report', async () => { + const markdown = renderBundleReportMarkdown( + await collectReport(repoDirs.before), + await collectReport(repoDirs.after), + await loadStats('before'), + await loadStats('after'), + { visualizerArtifactUrl: 'https://example.invalid/treemap' }, + ); + + await expect(markdown).toMatchFileSnapshot('./__snapshots__/render-md.md'); +}); + +test('fails loudly when the built output is missing', async () => { + await expect(collectReport(join(workDir, 'nonexistent'))).rejects.toThrow(); +}); diff --git a/packages-private/diagnostics-frontend-bundle/tsconfig.json b/packages-private/diagnostics-frontend-bundle/tsconfig.json new file mode 100644 index 0000000000..6672c40acb --- /dev/null +++ b/packages-private/diagnostics-frontend-bundle/tsconfig.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "strictFunctionTypes": true, + "strictNullChecks": true, + "esModuleInterop": true, + "skipLibCheck": true, + "noEmit": true, + "types": ["node"] + }, + "include": [ + "src/**/*.ts", + "test/**/*.ts" + ], + "exclude": [] +} diff --git a/packages-private/diagnostics-shared/eslint.config.js b/packages-private/diagnostics-shared/eslint.config.js new file mode 100644 index 0000000000..11bc648243 --- /dev/null +++ b/packages-private/diagnostics-shared/eslint.config.js @@ -0,0 +1,25 @@ +import tsParser from '@typescript-eslint/parser'; +import sharedConfig from '../../packages/shared/eslint.config.js'; + +// eslint-disable-next-line import/no-default-export +export default [ + ...sharedConfig, + { + ignores: [ + '**/node_modules', + '**/__snapshots__', + 'test/fixtures', + ], + }, + { + files: ['**/*.ts', '**/*.tsx'], + languageOptions: { + parserOptions: { + parser: tsParser, + project: ['./tsconfig.json'], + sourceType: 'module', + tsconfigRootDir: import.meta.dirname, + }, + }, + }, +]; diff --git a/packages-private/diagnostics-shared/package.json b/packages-private/diagnostics-shared/package.json new file mode 100644 index 0000000000..1fd0a14682 --- /dev/null +++ b/packages-private/diagnostics-shared/package.json @@ -0,0 +1,23 @@ +{ + "name": "diagnostics-shared", + "private": true, + "type": "module", + "exports": { + "./env": "./src/env.ts", + "./format": "./src/format.ts", + "./html": "./src/html.ts", + "./stats": "./src/stats.ts", + "./heap-snapshot": "./src/heap-snapshot/index.ts" + }, + "scripts": { + "eslint": "eslint './**/*.{js,jsx,ts,tsx}'", + "lint": "pnpm typecheck && pnpm eslint", + "test": "vitest run", + "typecheck": "tsc --noEmit" + }, + "devDependencies": { + "@types/node": "26.1.1", + "typescript": "5.9.3", + "vitest": "4.1.10" + } +} diff --git a/packages-private/diagnostics-shared/src/env.ts b/packages-private/diagnostics-shared/src/env.ts new file mode 100644 index 0000000000..0875cb2373 --- /dev/null +++ b/packages-private/diagnostics-shared/src/env.ts @@ -0,0 +1,40 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export function readIntegerEnv(name: string, defaultValue: number, min: number) { + const rawValue = process.env[name]; + if (rawValue == null || rawValue === '') return defaultValue; + if (!/^\d+$/.test(rawValue)) throw new Error(`${name} must be an integer`); + + const value = Number(rawValue); + if (!Number.isSafeInteger(value) || value < min) throw new Error(`${name} must be >= ${min}`); + return value; +} + +export function readBooleanEnv(name: string, defaultValue: boolean) { + const rawValue = process.env[name]; + if (rawValue == null || rawValue === '') return defaultValue; + if (rawValue === '1' || rawValue === 'true') return true; + if (rawValue === '0' || rawValue === 'false') return false; + throw new Error(`${name} must be one of: 1, 0, true, false`); +} + +/** + * 必須の環境変数を読む。未設定・空文字ならエラーにする。 + */ +export function readRequiredEnv(name: string) { + const rawValue = process.env[name]?.trim(); + if (rawValue == null || rawValue === '') throw new Error(`${name} must be set`); + return rawValue; +} + +/** + * 任意の環境変数を読む。未設定・空文字なら null を返す。 + */ +export function readOptionalEnv(name: string) { + const rawValue = process.env[name]?.trim(); + if (rawValue == null || rawValue === '') return null; + return rawValue; +} diff --git a/packages-private/diagnostics-shared/src/format.ts b/packages-private/diagnostics-shared/src/format.ts new file mode 100644 index 0000000000..75dc835e7b --- /dev/null +++ b/packages-private/diagnostics-shared/src/format.ts @@ -0,0 +1,116 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +const numberFormatter = new Intl.NumberFormat('en-US', { + maximumFractionDigits: 1, +}); + +export function escapeLatex(text: string) { + return text + .replaceAll('\\', '\\\\') + .replaceAll('{', '\\{') + .replaceAll('}', '\\}') + .replaceAll('%', '\\%'); +} + +export function escapeMdTableCell(value: string) { + return String(value).replaceAll('|', '\\|').replaceAll('\n', '
'); +} + +export function escapeHtml(value: unknown) { + return String(value ?? '') + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .replaceAll('\'', '''); +} + +export function formatNumber(value: number) { + return numberFormatter.format(value); +} + +export function formatBytes(value: number) { + if (value === 0) return '0 B'; + const units = ['B', 'KB', 'MB', 'GB']; + let unitIndex = 0; + let size = value; + while (size >= 1000 && unitIndex < units.length - 1) { + size /= 1000; + unitIndex += 1; + } + + const maximumFractionDigits = size >= 10 || unitIndex === 0 ? 0 : 1; + return `${numberFormatter.format(Number(size.toFixed(maximumFractionDigits)))} ${units[unitIndex]}`; +} + +/** + * KiB単位の値をMB表記にする。/proc 由来の値の表示に使う。 + */ +export function formatKiBAsMb(valueKiB: number | null | undefined) { + if (valueKiB == null || !Number.isFinite(valueKiB)) return '-'; + return `${formatNumber(valueKiB / 1000)} MB`; +} + +export function formatPercent(value: number) { + return `${formatNumber(value)}%`; +} + +export function formatMs(value: number | null | undefined) { + if (value == null || !Number.isFinite(value)) return '-'; + if (value >= 1_000) return `${formatNumber(value / 1_000)} s`; + return `${formatNumber(value)} ms`; +} + +export function formatSecondsAsMs(value: number | null | undefined) { + if (value == null || !Number.isFinite(value)) return '-'; + return formatMs(value * 1_000); +} + +/** + * 差分値を符号付きで整形する。`colorThreshold` 以上の変化があるときだけ色を付ける。 + */ +export function formatColoredDelta(delta: number, text: (value: number) => string, colorThreshold = 0) { + if (delta === 0) return text(0); + const sign = delta > 0 ? '+' : '-'; + if (Math.abs(delta) < colorThreshold) return `$\\text{${sign}${escapeLatex(text(Math.abs(delta)))}}$`; + const color = delta > 0 ? 'orange' : 'green'; + return `$\\color{${color}}{\\text{${sign}${escapeLatex(text(Math.abs(delta)))}}}$`; +} + +export function formatDeltaBytes(deltaBytes: number, colorThreshold = 0) { + return formatColoredDelta(deltaBytes, formatBytes, colorThreshold); +} + +export function formatDeltaPercent(deltaPercent: number, colorThreshold = 0) { + return formatColoredDelta(deltaPercent, formatPercent, colorThreshold); +} + +/** + * Markdownのテーブルセル内に置く差分パーセント。 + * LaTeX由来の `\%` がMarkdownのエスケープで食われるため二重エスケープする。 + */ +export function formatDeltaPercentInMdTable(deltaPercent: number, colorThreshold = 0) { + return formatDeltaPercent(deltaPercent, colorThreshold).replaceAll('\\%', '\\\\%'); +} + +export function calcAndFormatDeltaNumber(before: number | null | undefined, after: number | null | undefined, colorThreshold = 0) { + if (before == null || after == null) return '-'; + return formatColoredDelta(after - before, formatNumber, colorThreshold); +} + +export function calcAndFormatDeltaBytes(before: number | null | undefined, after: number | null | undefined, colorThreshold = 0) { + if (before == null || after == null) return '-'; + return formatDeltaBytes(after - before, colorThreshold); +} + +export function calcAndFormatDeltaPercent(before: number | null | undefined, after: number | null | undefined, colorThreshold = 0) { + if (before == null || before === 0 || after == null) return '-'; + return formatDeltaPercent((after - before) / before * 100, colorThreshold); +} + +export function calcAndFormatDeltaPercentInMdTable(before: number | null | undefined, after: number | null | undefined, colorThreshold = 0) { + return calcAndFormatDeltaPercent(before, after, colorThreshold).replaceAll('\\%', '\\\\%'); +} diff --git a/packages-private/diagnostics-shared/src/heap-snapshot/analyze.ts b/packages-private/diagnostics-shared/src/heap-snapshot/analyze.ts new file mode 100644 index 0000000000..b3443e1889 --- /dev/null +++ b/packages-private/diagnostics-shared/src/heap-snapshot/analyze.ts @@ -0,0 +1,198 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { classifyHeapSnapshotBreakdown, collapseHeapSnapshotBreakdowns } from './breakdown'; +import { + createEmptyHeapSnapshotData, + heapSnapshotBreakdownCategories, + type HeapSnapshotCategory, + type HeapSnapshotData, +} from './categories'; + +/** + * `.heapsnapshot` のJSONを、フィールドオフセットを解決した状態で扱うためのビュー。 + */ +type HeapSnapshotView = { + nodes: number[]; + edges: number[]; + strings: string[]; + nodeFieldCount: number; + edgeFieldCount: number; + nodeTypeNames: string[]; + edgeTypeNames: string[]; + typeOffset: number; + nameOffset: number; + selfSizeOffset: number; + edgeCountOffset: number; + edgeTypeOffset: number; + edgeNameOffset: number; + edgeToNodeOffset: number; + extraNativeBytes: number; +}; + +function requireOffsets(fields: string[], names: string[], what: string) { + const offsets = names.map(name => fields.indexOf(name)); + if (offsets.some(offset => offset < 0)) throw new Error(`Heap snapshot is missing required ${what} fields`); + return offsets; +} + +function parseHeapSnapshot(snapshot: any): HeapSnapshotView { + const meta = snapshot?.snapshot?.meta; + const { nodes, edges, strings } = snapshot ?? {}; + if (meta == null || !Array.isArray(nodes) || !Array.isArray(edges) || !Array.isArray(strings)) { + throw new Error('Invalid heap snapshot format'); + } + + const nodeFields = meta.node_fields; + if (!Array.isArray(nodeFields)) throw new Error('Invalid heap snapshot node fields'); + const edgeFields = meta.edge_fields; + if (!Array.isArray(edgeFields)) throw new Error('Invalid heap snapshot edge fields'); + + const [typeOffset, nameOffset, selfSizeOffset, edgeCountOffset] = requireOffsets(nodeFields, ['type', 'name', 'self_size', 'edge_count'], 'node'); + const [edgeTypeOffset, edgeNameOffset, edgeToNodeOffset] = requireOffsets(edgeFields, ['type', 'name_or_index', 'to_node'], 'edge'); + + const nodeTypeNames = meta.node_types?.[typeOffset]; + if (!Array.isArray(nodeTypeNames)) throw new Error('Invalid heap snapshot node types'); + const edgeTypeNames = meta.edge_types?.[edgeTypeOffset]; + if (!Array.isArray(edgeTypeNames)) throw new Error('Invalid heap snapshot edge types'); + + return { + nodes, + edges, + strings, + nodeFieldCount: nodeFields.length, + edgeFieldCount: edgeFields.length, + nodeTypeNames, + edgeTypeNames, + typeOffset, + nameOffset, + selfSizeOffset, + edgeCountOffset, + edgeTypeOffset, + edgeNameOffset, + edgeToNodeOffset, + extraNativeBytes: Number.isFinite(snapshot.snapshot.extra_native_bytes) ? snapshot.snapshot.extra_native_bytes : 0, + }; +} + +/** + * ノードごとのedge開始位置と、各ノードが何本のedgeから参照されているかを求める。 + * JS配列のelementsストアが専有されているか判定するために使う。 + */ +function indexEdges(view: HeapSnapshotView) { + const edgeStartIndexes = new Map(); + const retainerCounts = new Map(); + let edgeIndex = 0; + + for (let nodeIndex = 0; nodeIndex < view.nodes.length; nodeIndex += view.nodeFieldCount) { + edgeStartIndexes.set(nodeIndex, edgeIndex); + const edgeCount = view.nodes[nodeIndex + view.edgeCountOffset] ?? 0; + for (let i = 0; i < edgeCount; i++, edgeIndex += view.edgeFieldCount) { + const toNodeIndex = view.edges[edgeIndex + view.edgeToNodeOffset]; + retainerCounts.set(toNodeIndex, (retainerCounts.get(toNodeIndex) ?? 0) + 1); + } + } + + return { edgeStartIndexes, retainerCounts }; +} + +export function analyzeHeapSnapshot(snapshot: any, options: { breakdownTopN?: number } = {}): HeapSnapshotData { + const view = parseHeapSnapshot(snapshot); + const { nodes, edges, strings, nodeFieldCount, edgeFieldCount, nodeTypeNames, edgeTypeNames } = view; + + const nativeType = nodeTypeNames.indexOf('native'); + const codeType = nodeTypeNames.indexOf('code'); + const hiddenType = nodeTypeNames.indexOf('hidden'); + const stringTypes = new Set([ + nodeTypeNames.indexOf('string'), + nodeTypeNames.indexOf('concatenated string'), + nodeTypeNames.indexOf('sliced string'), + ]); + const internalEdgeType = edgeTypeNames.indexOf('internal'); + + const { categories, nodeCounts } = createEmptyHeapSnapshotData(); + const breakdowns = {} as Record>; + for (const category of heapSnapshotBreakdownCategories) { + breakdowns[category] = {}; + } + + const { edgeStartIndexes, retainerCounts } = indexEdges(view); + const jsArrayElementNodeIndexes = new Set(); + + function addCategoryValue(category: HeapSnapshotCategory, value: number, type: string, name: string, counted = true) { + if (value <= 0) return; + categories[category] += value; + const label = classifyHeapSnapshotBreakdown(category, type, name); + breakdowns[category][label] = (breakdowns[category][label] ?? 0) + value; + if (counted) nodeCounts[category]++; + } + + /** + * 配列オブジェクト自身のself_sizeには要素ストアが含まれないため、 + * その配列だけが参照しているelementsノードの分を JS arrays に加算する。 + */ + function addJsArrayElementSize(nodeIndex: number) { + const beginEdgeIndex = edgeStartIndexes.get(nodeIndex) ?? 0; + const edgeCount = nodes[nodeIndex + view.edgeCountOffset] ?? 0; + for (let i = 0, currentEdgeIndex = beginEdgeIndex; i < edgeCount; i++, currentEdgeIndex += edgeFieldCount) { + if (edges[currentEdgeIndex + view.edgeTypeOffset] !== internalEdgeType) continue; + if (strings[edges[currentEdgeIndex + view.edgeNameOffset]] !== 'elements') continue; + + const elementsNodeIndex = edges[currentEdgeIndex + view.edgeToNodeOffset]; + if ((retainerCounts.get(elementsNodeIndex) ?? 0) === 1) { + addCategoryValue('jsArrays', nodes[elementsNodeIndex + view.selfSizeOffset] ?? 0, 'array elements', 'Array elements'); + jsArrayElementNodeIndexes.add(elementsNodeIndex); + } + break; + } + } + + if (view.extraNativeBytes > 0) { + addCategoryValue('otherNonJsObjects', view.extraNativeBytes, 'extra native bytes', 'extra native bytes', false); + } + + for (let nodeIndex = 0; nodeIndex < nodes.length; nodeIndex += nodeFieldCount) { + const typeId = nodes[nodeIndex + view.typeOffset]; + const type = nodeTypeNames[typeId] ?? 'unknown'; + const name = strings[nodes[nodeIndex + view.nameOffset]] ?? ''; + const selfSize = nodes[nodeIndex + view.selfSizeOffset] ?? 0; + categories.total += selfSize; + nodeCounts.total++; + + if (typeId === hiddenType) { + addCategoryValue('systemObjects', selfSize, type, name); + } else if (typeId === nativeType) { + addCategoryValue(name === 'system / JSArrayBufferData' ? 'typedArrays' : 'otherNonJsObjects', selfSize, type, name); + } else if (typeId === codeType) { + addCategoryValue('code', selfSize, type, name); + } else if (stringTypes.has(typeId)) { + addCategoryValue('strings', selfSize, type, name); + } else if (name === 'Array') { + addCategoryValue('jsArrays', selfSize, type, name); + addJsArrayElementSize(nodeIndex); + } + } + + categories.total += view.extraNativeBytes; + + // 上のループで JS arrays に計上したelementsノードが確定してから、残りを Other JS objects に振り分ける + for (let nodeIndex = 0; nodeIndex < nodes.length; nodeIndex += nodeFieldCount) { + if (jsArrayElementNodeIndexes.has(nodeIndex)) continue; + + const typeId = nodes[nodeIndex + view.typeOffset]; + if (typeId === hiddenType || typeId === nativeType || typeId === codeType || stringTypes.has(typeId)) continue; + + const name = strings[nodes[nodeIndex + view.nameOffset]] ?? ''; + if (name === 'Array') continue; + + addCategoryValue('otherJsObjects', nodes[nodeIndex + view.selfSizeOffset] ?? 0, nodeTypeNames[typeId] ?? 'unknown', name); + } + + return { + categories, + nodeCounts, + breakdowns: collapseHeapSnapshotBreakdowns(breakdowns, options.breakdownTopN), + }; +} diff --git a/packages-private/diagnostics-shared/src/heap-snapshot/breakdown.ts b/packages-private/diagnostics-shared/src/heap-snapshot/breakdown.ts new file mode 100644 index 0000000000..0dd5ffe324 --- /dev/null +++ b/packages-private/diagnostics-shared/src/heap-snapshot/breakdown.ts @@ -0,0 +1,94 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { + defaultHeapSnapshotBreakdownTopN, + heapSnapshotBreakdownCategories, + type HeapSnapshotCategory, + type HeapSnapshotData, +} from './categories'; + +function sanitizeLabel(value: unknown, fallback = 'unknown') { + const label = String(value ?? '').replace(/\s+/g, ' ').trim(); + if (label === '') return fallback; + if (label.length <= 80) return label; + return `${label.slice(0, 77)}...`; +} + +/** + * ノードの type / name から、内訳テーブルに出す粒度のラベルを決める。 + */ +export function classifyHeapSnapshotBreakdown(category: HeapSnapshotCategory, type: string, name: string) { + switch (category) { + case 'strings': + return type; + + case 'jsArrays': + if (type === 'array elements') return 'Array elements'; + if (type === 'object' && name === 'Array') return 'Array objects'; + return sanitizeLabel(`${type}: ${name}`); + + case 'typedArrays': + if (name === 'system / JSArrayBufferData') return 'ArrayBuffer data'; + return sanitizeLabel(`${type}: ${name}`); + + case 'systemObjects': + if (name.startsWith('system /') || name.startsWith('(system ')) return sanitizeLabel(name); + return sanitizeLabel(`${type}: ${name}`, type); + + case 'otherJsObjects': + if (type === 'object') return sanitizeLabel(`object: ${name}`, 'object: unknown'); + return type; + + case 'otherNonJsObjects': + if (type === 'extra native bytes') return 'Extra native bytes'; + if (type === 'native') return sanitizeLabel(`native: ${name}`, 'native: unknown'); + return sanitizeLabel(`${type}: ${name}`, type); + + case 'code': { + const lowerName = name.toLowerCase(); + if (lowerName.includes('bytecode')) return 'bytecode'; + if (lowerName.includes('builtin')) return 'builtins'; + if (lowerName.includes('regexp')) return 'regexp code'; + if (lowerName.includes('stub')) return 'stubs'; + return sanitizeLabel(`code: ${name}`, 'code: unknown'); + } + + default: + return sanitizeLabel(`${type}: ${name}`, type); + } +} + +/** + * 内訳を上位N件に丸め、残りを `Other` にまとめる。 + */ +export function collapseHeapSnapshotBreakdown(breakdown: Record, topN = defaultHeapSnapshotBreakdownTopN) { + const entries = Object.entries(breakdown) + .filter(([, value]) => value > 0) + .toSorted((a, b) => b[1] - a[1]); + + const collapsed = Object.fromEntries(entries.slice(0, topN)); + const otherValue = entries.slice(topN).reduce((sum, [, value]) => sum + value, 0); + if (otherValue > 0) collapsed.Other = otherValue; + return collapsed; +} + +export function collapseHeapSnapshotBreakdowns( + breakdowns: Partial>>, + topN = defaultHeapSnapshotBreakdownTopN, +) { + const collapsed: NonNullable = {}; + for (const category of heapSnapshotBreakdownCategories) { + const categoryBreakdown = breakdowns[category]; + if (categoryBreakdown == null) continue; + + const collapsedCategory = collapseHeapSnapshotBreakdown(categoryBreakdown, topN); + if (Object.keys(collapsedCategory).length > 0) { + collapsed[category] = collapsedCategory; + } + } + + return collapsed; +} diff --git a/packages-private/diagnostics-shared/src/heap-snapshot/categories.ts b/packages-private/diagnostics-shared/src/heap-snapshot/categories.ts new file mode 100644 index 0000000000..4e7caed6d8 --- /dev/null +++ b/packages-private/diagnostics-shared/src/heap-snapshot/categories.ts @@ -0,0 +1,54 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +// Chrome DevTools の heap snapshot Statistics ビューと同じ分類になるように保つこと。 +export const heapSnapshotCategory = { + total: { label: 'Total', color: 'gray', colorHex: '#888888' }, + code: { label: 'Code', color: 'orange', colorHex: '#f28e2c' }, + strings: { label: 'Strings', color: 'red', colorHex: '#e15759' }, + jsArrays: { label: 'JS arrays', color: 'cyan', colorHex: '#76b7b2' }, + typedArrays: { label: 'Typed arrays', color: 'green', colorHex: '#59a14f' }, + systemObjects: { label: 'System objects', color: 'yellow', colorHex: '#edc949' }, + otherJsObjects: { label: 'Other JS objs', color: 'violet', colorHex: '#af7aa1' }, + otherNonJsObjects: { label: 'Other non-JS objs', color: 'pink', colorHex: '#ff9da7' }, +} as const satisfies Record; + +export type HeapSnapshotCategory = keyof typeof heapSnapshotCategory; + +export const heapSnapshotCategories = Object.keys(heapSnapshotCategory) as HeapSnapshotCategory[]; + +/** `total` は他カテゴリの合算ではなく全体量なので、内訳を扱うときは除外する */ +export const heapSnapshotBreakdownCategories = heapSnapshotCategories.filter(category => category !== 'total'); + +export type HeapSnapshotData = { + categories: Record; + nodeCounts: Record; + /** 内訳が空でないカテゴリだけが入る (`total` は内訳を持たない) */ + breakdowns?: Partial>>; +}; + +export type HeapSnapshotReport = { + summary: HeapSnapshotData; + samples: { + round: number; + data: HeapSnapshotData; + }[]; +}; + +export const defaultHeapSnapshotBreakdownTopN = 6; + +export function createEmptyHeapSnapshotData(): HeapSnapshotData { + const categories = {} as HeapSnapshotData['categories']; + const nodeCounts = {} as HeapSnapshotData['nodeCounts']; + for (const category of heapSnapshotCategories) { + categories[category] = 0; + nodeCounts[category] = 0; + } + return { + categories, + nodeCounts, + breakdowns: {}, + }; +} diff --git a/packages-private/diagnostics-shared/src/heap-snapshot/index.ts b/packages-private/diagnostics-shared/src/heap-snapshot/index.ts new file mode 100644 index 0000000000..0634efac87 --- /dev/null +++ b/packages-private/diagnostics-shared/src/heap-snapshot/index.ts @@ -0,0 +1,10 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export * from './analyze'; +export * from './breakdown'; +export * from './categories'; +export * from './render'; +export * from './summarize'; diff --git a/packages-private/diagnostics-shared/src/heap-snapshot/render.ts b/packages-private/diagnostics-shared/src/heap-snapshot/render.ts new file mode 100644 index 0000000000..7690c834c5 --- /dev/null +++ b/packages-private/diagnostics-shared/src/heap-snapshot/render.ts @@ -0,0 +1,177 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { + formatBytes, + formatDeltaBytes, + formatDeltaPercentInMdTable, + formatPercent, +} from '../format'; +import { mad, pairedDeltaSummary } from '../stats'; +import { + heapSnapshotCategories, + heapSnapshotCategory, + type HeapSnapshotCategory, + type HeapSnapshotReport, +} from './categories'; + +/** これ未満のバイト差分には色を付けない (0.1 MB) */ +const byteColorThreshold = 100_000; + +function categoryValue(report: HeapSnapshotReport, category: HeapSnapshotCategory) { + return report.summary.categories[category]; +} + +function categorySampleSpread(report: HeapSnapshotReport, category: HeapSnapshotCategory) { + const values = report.samples + .map(sample => sample.data.categories[category]) + .filter(value => Number.isFinite(value)); + if (values.length < 2) throw new Error(`Not enough samples for category ${category}`); + + return mad(values); +} + +function swatch(category: HeapSnapshotCategory) { + return `$\\color{${heapSnapshotCategory[category].color}}{\\rule{8pt}{8pt}}$ **${heapSnapshotCategory[category].label}**`; +} + +/** + * base / head のheap snapshotをカテゴリ別に比較するMarkdownテーブルを描画する。 + */ +export function renderHeapSnapshotTable(base: HeapSnapshotReport, head: HeapSnapshotReport) { + const lines = [ + '| Metric | Base | Head | Δ median | Δ MAD | Δ min | Δ max |', + '| --- | ---: | ---: | ---: | ---: | ---: | ---: |', + ]; + const baseTotal = categoryValue(base, 'total'); + const headTotal = categoryValue(head, 'total'); + + for (const category of heapSnapshotCategories) { + const baseValue = categoryValue(base, category); + const headValue = categoryValue(head, category); + const summary = pairedDeltaSummary(base.samples, head.samples, sample => sample.data.categories[category]); + const deltaColumns = `${formatBytes(summary.mad)} | ${formatDeltaBytes(summary.min, byteColorThreshold)} | ${formatDeltaBytes(summary.max, byteColorThreshold)}`; + + if (category === 'total') { + // Totalだけはばらつきと変化率も併記する + const percent = summary.median * 100 / baseValue; + const deltaMedian = `${formatDeltaBytes(summary.median, byteColorThreshold)}
${formatDeltaPercentInMdTable(percent, 0.1)}`; + const baseText = `${formatBytes(baseValue)}
± ${formatBytes(categorySampleSpread(base, category))}`; + const headText = `${formatBytes(headValue)}
± ${formatBytes(categorySampleSpread(head, category))}`; + lines.push(`| ${swatch(category)} | ${baseText} | ${headText} | ${deltaMedian} | ${deltaColumns} |`); + lines.push('| | | | | | | |'); + } else { + // 各カテゴリはTotalに占める割合の推移をdetailsで見せる + const basePercent = formatPercent((baseValue * 100) / baseTotal); + const headPercent = formatPercent((headValue * 100) / headTotal); + const metricText = `
${swatch(category)}${basePercent} → ${headPercent}
`; + lines.push(`| ${metricText} | ${formatBytes(baseValue)} | ${formatBytes(headValue)} | ${formatDeltaBytes(summary.median, byteColorThreshold)} | ${deltaColumns} |`); + } + } + + if (lines.length === 2) return null; + return lines.join('\n'); +} + +const sankeyChildMinRatio = 0.3; +const sankeyParentMinPercent = 10; + +function escapeCsvValue(value: string) { + return `"${String(value).replaceAll('"', '""')}"`; +} + +function formatSankeyPercentValue(value: number) { + const rounded = Math.round(value * 100) / 100; + if (rounded === 0 && value > 0) return '0.01'; + if (Number.isInteger(rounded)) return String(rounded); + return rounded.toFixed(2).replace(/0+$/, '').replace(/\.$/, ''); +} + +/** + * heap snapshotの構成比をmermaidのsankey図として描画する。 + * 全体に占める割合が小さいカテゴリ・内訳は `Other` にまとめる。 + */ +export function renderHeapSnapshotSankey(report: HeapSnapshotReport, title: string) { + const total = categoryValue(report, 'total'); + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (total == null || total <= 0) return null; + + const categories = heapSnapshotCategories + .filter(category => category !== 'total') + .map(category => { + const value = categoryValue(report, category); + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (value == null || value <= 0) return null; + + const breakdownEntries = Object.entries(report.summary.breakdowns?.[category] ?? {}) + .filter(([, childValue]) => Number.isFinite(childValue) && childValue > 0) + .toSorted((a, b) => b[1] - a[1]); + const breakdownTotal = breakdownEntries.reduce((sum, [, childValue]) => sum + childValue, 0); + const percent = (value * 100) / total; + const childEntries: [string, number][] = []; + let otherPercent = 0; + + if (breakdownTotal > 0 && percent > sankeyParentMinPercent) { + for (const [childName, childValue] of breakdownEntries) { + const childRatio = childValue / breakdownTotal; + if (childRatio >= sankeyChildMinRatio) { + childEntries.push([childName.replace(/^[^:]+:\s*/, ''), percent * childRatio]); + } else { + otherPercent += percent * childRatio; + } + } + + if (childEntries.length > 0 && otherPercent > 0) { + childEntries.push(['Other', otherPercent]); + } + } + + return { category, percent, childEntries }; + }) + .filter(value => value != null); + + if (categories.length === 0) return null; + + const nodeColors: Record = { + [title]: heapSnapshotCategory.total.colorHex, + Other: '#888888', + }; + for (const { category, childEntries } of categories) { + nodeColors[category] = heapSnapshotCategory[category].colorHex; + for (const [childName] of childEntries) { + nodeColors[childName] = heapSnapshotCategory[category].colorHex; + } + } + + const lines = [ + `
${title} heap snapshot composition`, + '', + '```mermaid', + `%%{init: ${JSON.stringify({ + sankey: { + showValues: false, + linkColor: 'target', + labelStyle: 'outlined', + nodeAlignment: 'center', + nodePadding: 10, + nodeColors, + }, + })}}%%`, + 'sankey-beta', + ]; + + for (const { category, percent, childEntries } of categories) { + const categoryLabel = heapSnapshotCategory[category].label; + lines.push(`${escapeCsvValue(title)},${escapeCsvValue(categoryLabel)},${formatSankeyPercentValue(percent)}`); + + for (const [childName, childPercent] of childEntries) { + lines.push(`${escapeCsvValue(categoryLabel)},${escapeCsvValue(childName)},${formatSankeyPercentValue(childPercent)}`); + } + } + + lines.push('```', '', '
'); + + return lines.join('\n'); +} diff --git a/packages-private/diagnostics-shared/src/heap-snapshot/summarize.ts b/packages-private/diagnostics-shared/src/heap-snapshot/summarize.ts new file mode 100644 index 0000000000..bbc2a6d203 --- /dev/null +++ b/packages-private/diagnostics-shared/src/heap-snapshot/summarize.ts @@ -0,0 +1,63 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { finiteMedian } from '../stats'; +import { collapseHeapSnapshotBreakdown } from './breakdown'; +import { + heapSnapshotBreakdownCategories, + heapSnapshotCategories, + type HeapSnapshotCategory, + type HeapSnapshotData, +} from './categories'; + +function isComplete(values: Partial>): values is Record { + return heapSnapshotCategories.every(category => values[category] != null); +} + +/** + * 複数ラウンド分のheap snapshotを、カテゴリ・内訳ごとの中央値にまとめる。 + * 全カテゴリ分の値が揃わなければ null を返す。 + */ +export function summarizeHeapSnapshotDataSamples( + samples: T[], + getData: (sample: T) => HeapSnapshotData | null | undefined, + options: { breakdownTopN?: number } = {}, +) { + const data = samples.map(getData); + + const categories: Partial = {}; + const nodeCounts: Partial = {}; + for (const category of heapSnapshotCategories) { + const categoryValue = finiteMedian(data.map(snapshot => snapshot?.categories?.[category])); + if (categoryValue != null) categories[category] = categoryValue; + + const nodeCountValue = finiteMedian(data.map(snapshot => snapshot?.nodeCounts?.[category])); + if (nodeCountValue != null) nodeCounts[category] = nodeCountValue; + } + + // 一部のカテゴリだけ欠けた状態で返すと、呼び出し側が完全な値として扱って + // undefined を描画してしまう。全カテゴリ揃っていなければサマリ自体を無しとする + if (!isComplete(categories) || !isComplete(nodeCounts)) return null; + + const breakdowns: NonNullable = {}; + for (const category of heapSnapshotBreakdownCategories) { + const childKeys = new Set(data.flatMap(snapshot => Object.keys(snapshot?.breakdowns?.[category] ?? {}))); + + const categoryBreakdown = {} as Record; + for (const childKey of childKeys) { + const value = finiteMedian(data.map(snapshot => snapshot?.breakdowns?.[category]?.[childKey])); + if (value != null) categoryBreakdown[childKey] = value; + } + + const collapsed = collapseHeapSnapshotBreakdown(categoryBreakdown, options.breakdownTopN); + if (Object.keys(collapsed).length > 0) breakdowns[category] = collapsed; + } + + return { + categories, + nodeCounts, + ...(Object.keys(breakdowns).length > 0 ? { breakdowns } : {}), + } satisfies HeapSnapshotData; +} diff --git a/packages-private/diagnostics-shared/src/html.ts b/packages-private/diagnostics-shared/src/html.ts new file mode 100644 index 0000000000..907c089656 --- /dev/null +++ b/packages-private/diagnostics-shared/src/html.ts @@ -0,0 +1,58 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { escapeHtml } from './format'; + +/** + * エスケープ済み、あるいは意図的にエスケープしない生のHTML断片。 + * + * ただの文字列と型で区別するためだけの存在ではなく、`html` が実行時に + * 「この値はもうエスケープしなくてよい」と判定するためのマーカーでもある。 + * 型だけのブランドにすると実行時に判定できず、結局エスケープ漏れを防げない。 + */ +export class Raw { + constructor(private readonly value: string) {} + + toString() { + return this.value; + } +} + +/** + * 文字列をエスケープせずそのまま埋め込む。 + * 呼び出しが差分に残るので、レビューで「なぜ生で入れてよいのか」を確認できる。 + */ +export function raw(value: string) { + return new Raw(value); +} + +function interpolate(value: unknown): string { + if (value instanceof Raw) return value.toString(); + // 配列をそのまま文字列化するとカンマ区切りで潰れるので、要素ごとに処理する + if (Array.isArray(value)) return value.map(interpolate).join(''); + if (value == null) return ''; + return escapeHtml(value); +} + +/** + * HTMLを組み立てるタグ付きテンプレート。補間値は既定でエスケープされる。 + * エスケープしたくない場合は `raw()` で包む必要があるため、 + * 「うっかり生のまま埋め込む」ことが起きない。 + */ +export function html(strings: TemplateStringsArray, ...values: unknown[]) { + let result = strings[0]; + for (let i = 0; i < values.length; i++) { + result += interpolate(values[i]) + strings[i + 1]; + } + return new Raw(result); +} + +/** + * 断片を指定の区切り文字で連結する。 + * `html` の配列補間は区切り無しで繋ぐので、改行などを挟みたいときはこちらを使う。 + */ +export function joinHtml(parts: readonly Raw[], separator: string) { + return new Raw(parts.map(part => part.toString()).join(separator)); +} diff --git a/packages-private/diagnostics-shared/src/stats.ts b/packages-private/diagnostics-shared/src/stats.ts new file mode 100644 index 0000000000..7516242c32 --- /dev/null +++ b/packages-private/diagnostics-shared/src/stats.ts @@ -0,0 +1,84 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export function median(values: number[]) { + const sorted = values.toSorted((a, b) => a - b); + const center = Math.floor(sorted.length / 2); + if (sorted.length % 2 === 1) return sorted[center]; + return Math.round((sorted[center - 1] + sorted[center]) / 2); +} + +export function mad(values: number[]) { + if (values.length < 2) throw new Error('Not enough samples to calculate MAD'); + + const center = median(values); + return median(values.map(value => Math.abs(value - center))); +} + +/** + * 有限値のみを対象に中央値を求める。有限値が1つも無い場合は `defaultValue` を返す。 + */ +export function finiteMedian(values: (number | null | undefined)[]): number | null; +export function finiteMedian(values: (number | null | undefined)[], defaultValue: number): number; +export function finiteMedian(values: (number | null | undefined)[], defaultValue: number | null = null) { + const finiteValues = values.filter(value => Number.isFinite(value)) as number[]; + if (finiteValues.length === 0) return defaultValue; + return median(finiteValues); +} + +/** + * サンプルのばらつき (MAD) を求める。サンプルが2つ未満で求められない場合は null を返す。 + */ +export function sampleSpread(values: (number | null | undefined)[]) { + const finiteValues = values.filter(value => Number.isFinite(value)) as number[]; + if (finiteValues.length < 2) return null; + return mad(finiteValues); +} + +type RoundedSample = { round: number }; + +function indexByRound(samples: T[]) { + const samplesByRound = new Map(); + for (const sample of samples) { + // 負のroundはwarmupを表すため対象外 + if (sample.round <= 0) continue; + samplesByRound.set(sample.round, sample); + } + return samplesByRound; +} + +/** + * base / head を同じroundどうしで突き合わせ、その差分の分布を要約する。 + * 実行順による揺らぎの影響を抑えるため、単純な集計値どうしの引き算ではなくペア差分を使う。 + */ +export function pairedDeltaSummary(baseSamples: T[], headSamples: T[], getValue: (sample: T) => number | null | undefined) { + const baseSamplesByRound = indexByRound(baseSamples); + const headSamplesByRound = indexByRound(headSamples); + const values: number[] = []; + + for (const [round, baseSample] of baseSamplesByRound) { + const headSample = headSamplesByRound.get(round); + if (headSample == null) continue; + + const baseValue = getValue(baseSample); + const headValue = getValue(headSample); + if (baseValue == null || headValue == null) continue; + + values.push(headValue - baseValue); + } + + // 対応するroundが1つも無いと中央値も最小/最大も定義できない。 + // 静かにNaNやInfinityをレポートに載せるより、比較が成立していないと分かる形で落とす + if (values.length === 0) throw new Error('No paired samples to compare: base and head have no rounds in common'); + + return { + median: median(values), + // 1サンプルでは中央値からの偏差が常に0になる (mad() は統計として無意味なので拒否する) + mad: values.length < 2 ? 0 : mad(values), + min: Math.min(...values), + max: Math.max(...values), + samples: values.length, + }; +} diff --git a/packages-private/diagnostics-shared/test/format.test.ts b/packages-private/diagnostics-shared/test/format.test.ts new file mode 100644 index 0000000000..e391d86283 --- /dev/null +++ b/packages-private/diagnostics-shared/test/format.test.ts @@ -0,0 +1,102 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { describe, expect, test } from 'vitest'; +import { + calcAndFormatDeltaPercent, + calcAndFormatDeltaPercentInMdTable, + escapeHtml, + escapeLatex, + escapeMdTableCell, + formatBytes, + formatColoredDelta, + formatKiBAsMb, + formatMs, + formatNumber, +} from '../src/format'; + +describe('formatBytes', () => { + // 1024ではなく1000区切り。単位が2桁以上なら小数を落とす + test.each([ + [0, '0 B'], + [999, '999 B'], + [1_000, '1 KB'], + [1_500, '1.5 KB'], + [15_000, '15 KB'], + [1_234_567, '1.2 MB'], + [12_345_678, '12 MB'], + [1_500_000_000, '1.5 GB'], + [1_500_000_000_000, '1,500 GB'], + ])('formats %i as %s', (input, expected) => { + expect(formatBytes(input)).toBe(expected); + }); +}); + +describe('formatColoredDelta', () => { + test('leaves zero uncoloured and unsigned', () => { + expect(formatColoredDelta(0, formatNumber)).toBe('0'); + }); + + test('colours growth orange and shrinkage green', () => { + expect(formatColoredDelta(5, formatNumber)).toBe('$\\color{orange}{\\text{+5}}$'); + expect(formatColoredDelta(-5, formatNumber)).toBe('$\\color{green}{\\text{-5}}$'); + }); + + test('omits colour below the threshold but keeps the sign', () => { + expect(formatColoredDelta(5, formatNumber, 10)).toBe('$\\text{+5}$'); + }); +}); + +describe('escaping', () => { + test('escapeHtml covers the five metacharacters', () => { + expect(escapeHtml(`&<>"'`)).toBe('&<>"''); + }); + + test('escapeHtml maps nullish to an empty string', () => { + expect(escapeHtml(null)).toBe(''); + expect(escapeHtml(undefined)).toBe(''); + }); + + test('escapeLatex escapes braces and percent', () => { + expect(escapeLatex('100% {x}')).toBe('100\\% \\{x\\}'); + }); + + test('escapeMdTableCell keeps pipes and newlines from breaking the table', () => { + expect(escapeMdTableCell('a|b\nc')).toBe('a\\|b
c'); + }); +}); + +describe('percent helpers', () => { + // before が0だと変化率そのものが定義できない + test('returns a placeholder when the baseline is zero or missing', () => { + expect(calcAndFormatDeltaPercent(0, 10)).toBe('-'); + expect(calcAndFormatDeltaPercent(null, 10)).toBe('-'); + expect(calcAndFormatDeltaPercent(10, null)).toBe('-'); + }); + + // 0になったのは「消えた」という有効な結果なので、隠さず -100% として出す + test('formats a drop to zero as -100%', () => { + expect(calcAndFormatDeltaPercent(10, 0)).toBe('$\\color{green}{\\text{-100\\%}}$'); + }); + + // Markdownのテーブルセル内ではLaTeXの \% がさらに食われるため二重にする + test('doubles the percent escape for markdown table cells', () => { + expect(calcAndFormatDeltaPercentInMdTable(100, 110)).toContain('\\\\%'); + expect(calcAndFormatDeltaPercent(100, 110)).not.toContain('\\\\%'); + }); +}); + +describe('nullish handling', () => { + test('formatKiBAsMb and formatMs render a dash for missing values', () => { + expect(formatKiBAsMb(null)).toBe('-'); + expect(formatKiBAsMb(Number.NaN)).toBe('-'); + expect(formatMs(undefined)).toBe('-'); + }); + + test('formatMs switches to seconds past 1000ms', () => { + expect(formatMs(999)).toBe('999 ms'); + expect(formatMs(1_500)).toBe('1.5 s'); + }); +}); diff --git a/packages-private/diagnostics-shared/test/html.test.ts b/packages-private/diagnostics-shared/test/html.test.ts new file mode 100644 index 0000000000..6e9dbc48b0 --- /dev/null +++ b/packages-private/diagnostics-shared/test/html.test.ts @@ -0,0 +1,63 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { describe, expect, test } from 'vitest'; +import { html, joinHtml, raw, Raw } from '../src/html'; + +describe('html', () => { + test('escapes interpolated values by default', () => { + const value = ''; + expect(String(html`

${value}

`)).toBe('

<script>alert(1)</script>

'); + }); + + test('escapes values used in attribute position', () => { + const url = 'x">'; + expect(String(html``)).toBe(''); + }); + + test('leaves the static parts of the template untouched', () => { + expect(String(html`

x

`)).toBe('

x

'); + }); + + test('renders nullish values as an empty string', () => { + expect(String(html`

${null}${undefined}

`)).toBe('

'); + }); + + test('does not double-escape nested fragments', () => { + const inner = html`${'a&b'}`; + expect(String(html`

${inner}

`)).toBe('

a&b

'); + }); + + // 配列をそのまま文字列化するとカンマ区切りで潰れる。実際に過去これで表が壊れた + test('joins arrays without separators instead of stringifying them', () => { + const items = [html`
  • 1
  • `, html`
  • 2
  • `]; + expect(String(html`
      ${items}
    `)).toBe('
    • 1
    • 2
    '); + }); + + test('escapes array elements that are not fragments', () => { + expect(String(html`

    ${['', '']}

    `)).toBe('

    <a><b>

    '); + }); +}); + +describe('raw', () => { + test('embeds trusted markup without escaping', () => { + expect(String(html``)).toBe(''); + }); + + test('produces a Raw fragment', () => { + expect(raw('x')).toBeInstanceOf(Raw); + expect(html`x`).toBeInstanceOf(Raw); + }); +}); + +describe('joinHtml', () => { + test('joins fragments with the given separator', () => { + expect(String(joinHtml([html`
  • 1
  • `, html`
  • 2
  • `], '\n'))).toBe('
  • 1
  • \n
  • 2
  • '); + }); + + test('returns an empty fragment for an empty list', () => { + expect(String(joinHtml([], '\n'))).toBe(''); + }); +}); diff --git a/packages-private/diagnostics-shared/test/stats.test.ts b/packages-private/diagnostics-shared/test/stats.test.ts new file mode 100644 index 0000000000..1d57f0d67d --- /dev/null +++ b/packages-private/diagnostics-shared/test/stats.test.ts @@ -0,0 +1,109 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { describe, expect, test } from 'vitest'; +import { finiteMedian, mad, median, pairedDeltaSummary, sampleSpread } from '../src/stats'; + +describe('median', () => { + test('takes the middle value of an odd-length sample', () => { + expect(median([3, 1, 2])).toBe(2); + }); + + // 偶数長では平均を整数に丸める。KiB単位の整数値を扱う前提のため + test('rounds the average of an even-length sample', () => { + expect(median([1, 2])).toBe(2); + expect(median([1, 4])).toBe(3); + }); +}); + +describe('mad', () => { + test('measures the median absolute deviation', () => { + expect(mad([1, 1, 1])).toBe(0); + expect(mad([1, 2, 3])).toBe(1); + }); + + test('refuses to compute from a single sample', () => { + expect(() => mad([1])).toThrow(); + }); +}); + +describe('finiteMedian', () => { + test('ignores non-finite entries', () => { + expect(finiteMedian([1, null, undefined, Number.NaN, 3])).toBe(2); + }); + + test('returns null by default when nothing is finite', () => { + expect(finiteMedian([null, undefined])).toBeNull(); + }); + + test('returns the supplied default when nothing is finite', () => { + expect(finiteMedian([null, undefined], 0)).toBe(0); + }); +}); + +describe('sampleSpread', () => { + test('needs at least two finite samples', () => { + expect(sampleSpread([1])).toBeNull(); + expect(sampleSpread([1, null])).toBeNull(); + expect(sampleSpread([1, 3])).toBe(1); + }); +}); + +describe('pairedDeltaSummary', () => { + const base = [ + { round: 1, value: 100 }, + { round: 2, value: 200 }, + { round: 3, value: 300 }, + ]; + + test('compares base and head within the same round', () => { + const head = [ + { round: 1, value: 110 }, + { round: 2, value: 230 }, + { round: 3, value: 320 }, + ]; + + expect(pairedDeltaSummary(base, head, sample => sample.value)).toStrictEqual({ + median: 20, + mad: 10, + min: 10, + max: 30, + samples: 3, + }); + }); + + test('drops rounds that only one side has', () => { + const head = [ + { round: 1, value: 110 }, + { round: 2, value: 230 }, + { round: 9, value: 999 }, + ]; + + expect(pairedDeltaSummary(base, head, sample => sample.value).samples).toBe(2); + }); + + // 1サンプルでも中央値・最小・最大は定まる (偏差は常に0) + test('summarizes a single paired round without treating MAD as an error', () => { + expect(pairedDeltaSummary([base[0]], [{ round: 1, value: 130 }], sample => sample.value)).toStrictEqual({ + median: 30, + mad: 0, + min: 30, + max: 30, + samples: 1, + }); + }); + + test('fails loudly when no round is shared', () => { + expect(() => pairedDeltaSummary(base, [{ round: 9, value: 1 }], sample => sample.value)).toThrow(/no rounds in common/); + }); + + // 負のroundはwarmupを表すので集計に混ぜない + test('ignores warmup rounds', () => { + const warmupBase = [{ round: -1, value: 0 }, ...base]; + const warmupHead = [{ round: -1, value: 9999 }, { round: 1, value: 110 }, { round: 2, value: 230 }, { round: 3, value: 320 }]; + + expect(pairedDeltaSummary(warmupBase, warmupHead, sample => sample.value).samples).toBe(3); + }); +}); diff --git a/packages-private/diagnostics-shared/tsconfig.json b/packages-private/diagnostics-shared/tsconfig.json new file mode 100644 index 0000000000..6672c40acb --- /dev/null +++ b/packages-private/diagnostics-shared/tsconfig.json @@ -0,0 +1,20 @@ +{ + "$schema": "https://json.schemastore.org/tsconfig", + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "strictFunctionTypes": true, + "strictNullChecks": true, + "esModuleInterop": true, + "skipLibCheck": true, + "noEmit": true, + "types": ["node"] + }, + "include": [ + "src/**/*.ts", + "test/**/*.ts" + ], + "exclude": [] +} diff --git a/packages/backend/scripts/measure-memory.mts b/packages/backend/scripts/measure-memory.mts deleted file mode 100644 index a95eb3075c..0000000000 --- a/packages/backend/scripts/measure-memory.mts +++ /dev/null @@ -1,303 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { ChildProcess, fork } from 'node:child_process'; -import { dirname, join, resolve } from 'node:path'; -import { tmpdir } from 'node:os'; -import * as fs from 'node:fs/promises'; -import { analyzeHeapSnapshot, defaultHeapSnapshotBreakdownTopN, type HeapSnapshotData } from '../../../.github/scripts/heap-snapshot-util.mts'; -import { measureMemoryUntilStable } from '../../../.github/scripts/memory-stability-util.mts'; - -const backendDir = process.env.MK_MEMORY_BACKEND_DIR == null || process.env.MK_MEMORY_BACKEND_DIR === '' - ? join(import.meta.dirname, '..') - : resolve(process.env.MK_MEMORY_BACKEND_DIR); - -function readIntegerEnv(name, defaultValue, min) { - const rawValue = process.env[name]; - if (rawValue == null || rawValue === '') return defaultValue; - if (!/^\d+$/.test(rawValue)) throw new Error(`${name} must be an integer`); - - const value = Number(rawValue); - if (!Number.isSafeInteger(value) || value < min) throw new Error(`${name} must be >= ${min}`); - return value; -} - -function readBooleanEnv(name, defaultValue) { - const rawValue = process.env[name]; - if (rawValue == null || rawValue === '') return defaultValue; - if (rawValue === '1' || rawValue === 'true') return true; - if (rawValue === '0' || rawValue === 'false') return false; - throw new Error(`${name} must be one of: 1, 0, true, false`); -} - -const STARTUP_TIMEOUT = readIntegerEnv('MK_MEMORY_STARTUP_TIMEOUT_MS', 120000, 1); // Timeout for server startup -const IPC_TIMEOUT = readIntegerEnv('MK_MEMORY_IPC_TIMEOUT_MS', 30000, 1); // Timeout for IPC responses -const HEAP_SNAPSHOT = readBooleanEnv('MK_MEMORY_HEAP_SNAPSHOT', false); -const HEAP_SNAPSHOT_TIMEOUT = readIntegerEnv('MK_MEMORY_HEAP_SNAPSHOT_TIMEOUT_MS', 120000, 1); -const HEAP_SNAPSHOT_BREAKDOWN_TOP_N = readIntegerEnv('MK_MEMORY_HEAP_SNAPSHOT_BREAKDOWN_TOP_N', defaultHeapSnapshotBreakdownTopN, 1); -const HEAP_SNAPSHOT_SAVE_PATH = process.env.MK_MEMORY_HEAP_SNAPSHOT_SAVE_PATH; - -const procStatusKeys = ['VmPeak', 'VmSize', 'VmHWM', 'VmRSS', 'VmData', 'VmStk', 'VmExe', 'VmLib', 'VmPTE', 'VmSwap'] as const; -const smapsRollupKeys = ['Pss', 'Shared_Clean', 'Shared_Dirty', 'Private_Clean', 'Private_Dirty', 'Swap', 'SwapPss'] as const; - -type GcMessage = 'gc ok' | 'gc unavailable'; -type RuntimeMemoryUsageMessage = { - type: 'memory usage'; - value: NodeJS.MemoryUsage; -}; -type HeapSnapshotMessage = { - type: 'heap snapshot'; - path?: string; -}; -type HeapSnapshotErrorMessage = { - type: 'heap snapshot error'; - message: string; -}; -type HeapSnapshotResponseMessage = HeapSnapshotMessage | HeapSnapshotErrorMessage; - -function parseMemoryFile(content: string, keys: KS, path: string): Record { - const result = {} as Record; - for (const _key of keys) { - const key = _key as KS[number]; - const match = content.match(new RegExp(`${key}:\\s+(\\d+)\\s+kB`)); - if (match) { - result[key] = parseInt(match[1], 10); - } else { - throw new Error(`Failed to parse ${key} from ${path}`); - } - } - return result; -} - -function bytesToKiB(value: number) { - return Math.round(value / 1024); -} - -async function getMemoryUsage(pid: number) { - const path = `/proc/${pid}/status`; - const status = await fs.readFile(path, 'utf-8'); - return parseMemoryFile(status, procStatusKeys, path); -} - -async function getSmapsRollupMemoryUsage(pid: number) { - const path = `/proc/${pid}/smaps_rollup`; - const smapsRollup = await fs.readFile(path, 'utf-8'); - return parseMemoryFile(smapsRollup, smapsRollupKeys, path); -} - -function isRecord(value: unknown): value is Record { - return value != null && typeof value === 'object'; -} - -function isGcMessage(message: unknown): message is GcMessage { - return message === 'gc ok' || message === 'gc unavailable'; -} - -function isRuntimeMemoryUsageMessage(message: unknown): message is RuntimeMemoryUsageMessage { - return isRecord(message) && message.type === 'memory usage' && isRecord(message.value); -} - -function isHeapSnapshotResponseMessage(message: unknown): message is HeapSnapshotResponseMessage { - if (!isRecord(message)) return false; - if (message.type === 'heap snapshot') return true; - return message.type === 'heap snapshot error' && typeof message.message === 'string'; -} - -function waitForMessage(serverProcess: ChildProcess, predicate: (message: unknown) => message is T, description: string, timeout = IPC_TIMEOUT) { - return new Promise((resolve, reject) => { - const timer = globalThis.setTimeout(() => { - serverProcess.off('message', onMessage); - reject(new Error(`Timed out waiting for ${description}`)); - }, timeout); - - const onMessage = (message: unknown) => { - if (!predicate(message)) return; - globalThis.clearTimeout(timer); - serverProcess.off('message', onMessage); - resolve(message); - }; - - serverProcess.on('message', onMessage); - }); -} - -async function getRuntimeMemoryUsage(serverProcess: ChildProcess) { - const response = waitForMessage( - serverProcess, - isRuntimeMemoryUsageMessage, - 'memory usage', - ); - - serverProcess.send('memory usage'); - - const message = await response; - const memoryUsage = message.value; - - return { - HeapTotal: bytesToKiB(memoryUsage.heapTotal), - HeapUsed: bytesToKiB(memoryUsage.heapUsed), - External: bytesToKiB(memoryUsage.external), - ArrayBuffers: bytesToKiB(memoryUsage.arrayBuffers), - }; -} - -async function getHeapSnapshotStatistics(serverProcess: ChildProcess): Promise { - if (!HEAP_SNAPSHOT) return null; - - const snapshotPath = join(tmpdir(), `misskey-backend-heap-${process.pid}-${serverProcess.pid}-${Date.now()}.heapsnapshot`); - const response = waitForMessage( - serverProcess, - isHeapSnapshotResponseMessage, - 'heap snapshot', - HEAP_SNAPSHOT_TIMEOUT, - ); - - serverProcess.send({ - type: 'heap snapshot', - path: snapshotPath, - }); - - const message = await response; - if (message.type === 'heap snapshot error') { - throw new Error(`Failed to write heap snapshot: ${message.message}`); - } - - const writtenPath = typeof message.path === 'string' ? message.path : snapshotPath; - - try { - if (HEAP_SNAPSHOT_SAVE_PATH != null && HEAP_SNAPSHOT_SAVE_PATH !== '') { - await fs.mkdir(dirname(HEAP_SNAPSHOT_SAVE_PATH), { recursive: true }); - await fs.copyFile(writtenPath, HEAP_SNAPSHOT_SAVE_PATH); - } - - const snapshot = JSON.parse(await fs.readFile(writtenPath, 'utf-8')); - return analyzeHeapSnapshot(snapshot, { breakdownTopN: HEAP_SNAPSHOT_BREAKDOWN_TOP_N }); - } finally { - await fs.unlink(writtenPath).catch(err => { - process.stderr.write(`Failed to delete heap snapshot ${writtenPath}: ${err.message}\n`); - }); - } -} - -async function getAllMemoryUsage(serverProcess: ChildProcess) { - const pid = serverProcess.pid!; - const stableSmapsRollup = await measureMemoryUntilStable( - () => getSmapsRollupMemoryUsage(pid), - ); - - return { - memoryUsage: { - ...await getMemoryUsage(pid), - ...stableSmapsRollup.memoryUsage, - ...await getRuntimeMemoryUsage(serverProcess), - }, - stability: stableSmapsRollup.stability, - }; -} - -async function measureMemory() { - const serverProcess = fork(join(backendDir, 'built/entry.js'), [], { - cwd: backendDir, - env: { - ...process.env, - NODE_ENV: 'production', - MK_DISABLE_CLUSTERING: '1', - MK_ONLY_SERVER: '1', - MK_NO_DAEMONS: '1', - }, - stdio: ['pipe', 'pipe', 'pipe', 'ipc'], - execArgv: [...process.execArgv, '--expose-gc'], - }); - - const serverReady = waitForMessage( - serverProcess, - (message): message is 'ok' => message === 'ok', - 'server startup', - STARTUP_TIMEOUT, - ); - - serverProcess.stdout?.on('data', (data) => { - process.stderr.write(`[server stdout] ${data}`); - }); - - serverProcess.stderr?.on('data', (data) => { - process.stderr.write(`[server stderr] ${data}`); - }); - - serverProcess.on('error', (err) => { - process.stderr.write(`[server error] ${err}\n`); - }); - - async function triggerGc() { - const ok = waitForMessage( - serverProcess, - isGcMessage, - 'GC completion', - ); - - serverProcess.send('gc'); - - const message = await ok; - if (message === 'gc unavailable') { - throw new Error('GC is unavailable. Start the process with --expose-gc to enable this feature.'); - } - } - - const startupStartTime = Date.now(); - try { - await serverReady; - } catch (err) { - serverProcess.kill('SIGTERM'); - throw err; - } - - const startupTime = Date.now() - startupStartTime; - process.stderr.write(`Server started in ${startupTime}ms\n`); - - await triggerGc(); - - const afterGc = await getAllMemoryUsage(serverProcess); - process.stderr.write(`Memory ${afterGc.stability.converged ? 'stabilized' : 'did not stabilize'} after ${afterGc.stability.readingCount} readings over ${Math.round(afterGc.stability.elapsedMs)}ms\n`); - - const heapSnapshotAfterGc = await getHeapSnapshotStatistics(serverProcess); - - const serverExited = new Promise(resolve => { - const timer = globalThis.setTimeout(() => { - serverProcess.kill('SIGKILL'); - resolve(); - }, 10000); - serverProcess.once('exit', () => { - globalThis.clearTimeout(timer); - resolve(); - }); - }); - serverProcess.kill('SIGTERM'); - await serverExited; - - return { - timestamp: new Date().toISOString(), - phases: { - afterGc: { - memoryUsage: afterGc.memoryUsage, - memoryStability: afterGc.stability, - heapSnapshot: heapSnapshotAfterGc, - }, - }, - }; -} - -export type MemorySample = Awaited>; - -async function main() { - console.log(JSON.stringify(await measureMemory(), null, 2)); -} - -main().catch((err) => { - console.error(JSON.stringify({ - error: err.message, - timestamp: new Date().toISOString(), - })); - process.exit(1); -}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8f82d6b63f..53a344d0e5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -70,6 +70,126 @@ importers: specifier: npm:@typescript/typescript6@6.0.2 version: '@typescript/typescript6@6.0.2' + packages-private/changelog-checker: + devDependencies: + '@types/mdast': + specifier: 4.0.4 + version: 4.0.4 + '@types/node': + specifier: 26.1.1 + version: 26.1.1 + '@vitest/coverage-v8': + specifier: 4.1.10 + version: 4.1.10(@vitest/browser@4.1.10)(vitest@4.1.10) + mdast-util-to-string: + specifier: 4.0.0 + version: 4.0.0 + remark: + specifier: 15.0.1 + version: 15.0.1 + remark-parse: + specifier: 11.0.0 + version: 11.0.0 + tsx: + specifier: 4.23.1 + version: 4.23.1 + unified: + specifier: 11.0.5 + version: 11.0.5 + vitest: + specifier: 4.1.10 + version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.1.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(happy-dom@20.10.6(bufferutil@4.1.0)(utf-8-validate@6.0.6))(msw@2.15.0(@types/node@26.1.1)(typescript@7.0.2))(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.1)) + + packages-private/diagnostics-backend: + dependencies: + diagnostics-shared: + specifier: workspace:* + version: link:../diagnostics-shared + execa: + specifier: 9.6.1 + version: 9.6.1 + devDependencies: + '@types/node': + specifier: 26.1.1 + version: 26.1.1 + '@types/pg': + specifier: 8.20.0 + version: 8.20.0 + ioredis: + specifier: 5.11.1 + version: 5.11.1 + pg: + specifier: 8.22.0 + version: 8.22.0 + tsx: + specifier: 4.23.1 + version: 4.23.1 + typescript: + specifier: 5.9.3 + version: 5.9.3 + vitest: + specifier: 4.1.10 + version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.1.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(happy-dom@20.10.6(bufferutil@4.1.0)(utf-8-validate@6.0.6))(msw@2.15.0(@types/node@26.1.1)(typescript@5.9.3))(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.1)) + + packages-private/diagnostics-frontend-browser: + dependencies: + diagnostics-shared: + specifier: workspace:* + version: link:../diagnostics-shared + devDependencies: + '@types/node': + specifier: 26.1.1 + version: 26.1.1 + frontend: + specifier: workspace:* + version: link:../../packages/frontend + playwright: + specifier: 1.61.1 + version: 1.61.1 + tsx: + specifier: 4.23.1 + version: 4.23.1 + typescript: + specifier: 5.9.3 + version: 5.9.3 + vitest: + specifier: 4.1.10 + version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.1.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(happy-dom@20.10.6(bufferutil@4.1.0)(utf-8-validate@6.0.6))(msw@2.15.0(@types/node@26.1.1)(typescript@5.9.3))(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.1)) + + packages-private/diagnostics-frontend-bundle: + dependencies: + diagnostics-shared: + specifier: workspace:* + version: link:../diagnostics-shared + devDependencies: + '@types/node': + specifier: 26.1.1 + version: 26.1.1 + tsx: + specifier: 4.23.1 + version: 4.23.1 + typescript: + specifier: 5.9.3 + version: 5.9.3 + vite: + specifier: 8.1.4 + version: 8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.1) + vitest: + specifier: 4.1.10 + version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.1.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(happy-dom@20.10.6(bufferutil@4.1.0)(utf-8-validate@6.0.6))(msw@2.15.0(@types/node@26.1.1)(typescript@5.9.3))(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.1)) + + packages-private/diagnostics-shared: + devDependencies: + '@types/node': + specifier: 26.1.1 + version: 26.1.1 + typescript: + specifier: 5.9.3 + version: 5.9.3 + vitest: + specifier: 4.1.10 + version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.1.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(happy-dom@20.10.6(bufferutil@4.1.0)(utf-8-validate@6.0.6))(msw@2.15.0(@types/node@26.1.1)(typescript@5.9.3))(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.1)) + packages/backend: dependencies: '@aws-sdk/client-s3': @@ -504,10 +624,10 @@ importers: version: 5.1.0 vite: specifier: 8.1.4 - version: 8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.0) + version: 8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.1) vitest: specifier: 4.1.10 - version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.1.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(happy-dom@20.10.6(bufferutil@4.1.0)(utf-8-validate@6.0.6))(msw@2.15.0(@types/node@26.1.1)(@typescript/typescript6@6.0.2))(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.0)) + version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.1.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(happy-dom@20.10.6(bufferutil@4.1.0)(utf-8-validate@6.0.6))(msw@2.15.0(@types/node@26.1.1)(@typescript/typescript6@6.0.2))(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.1)) vitest-mock-extended: specifier: 4.0.0 version: 4.0.0(@typescript/typescript6@6.0.2)(vitest@4.1.10) @@ -944,7 +1064,7 @@ importers: version: 1.1.5 vite: specifier: 8.1.4 - version: 8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.0) + version: 8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.1) devDependencies: '@types/estree': specifier: 1.0.9 @@ -1263,7 +1383,7 @@ importers: version: 0.33.0 vitest: specifier: 4.1.10 - version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.1.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(happy-dom@20.10.6(bufferutil@4.1.0)(utf-8-validate@6.0.6))(msw@2.15.0(@types/node@26.1.1)(@typescript/typescript6@6.0.2))(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.0)) + version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.1.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(happy-dom@20.10.6(bufferutil@4.1.0)(utf-8-validate@6.0.6))(msw@2.15.0(@types/node@26.1.1)(@typescript/typescript6@6.0.2))(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.1)) vitest-websocket-mock: specifier: 0.7.0 version: 0.7.0(vitest@4.1.10) @@ -7772,6 +7892,9 @@ packages: remark-stringify@11.0.0: resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} + remark@15.0.1: + resolution: {integrity: sha512-Eht5w30ruCXgFmxVUSlNWQ9iiimq07URKeFS3hNc8cUWy1llX4KDWfyEDZRycMc+znsN9Ux5/tJ/BFdgdOwA3A==} + rename@1.0.4: resolution: {integrity: sha512-YMM6Fn3lrFOCjhORKjj+z/yizj8WSzv3F3YUlpJA20fteWCb0HbJU19nvuRBPUM5dWgxJcHP+kix3M+5NowJyA==} @@ -8674,6 +8797,11 @@ packages: engines: {node: '>=18.0.0'} hasBin: true + tsx@4.23.1: + resolution: {integrity: sha512-GQHnkIfxyx1wYCOS/wonik5MVRZU9hi1TEZmzGZSCJB1y9YgoZ8H6itNE/u4suE+yLmOzuE4E5S4TZ/ZX2wcWQ==} + engines: {node: '>=18.0.0'} + hasBin: true + tsyringe@4.10.0: resolution: {integrity: sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw==} engines: {node: '>= 6.0.0'} @@ -12239,13 +12367,27 @@ snapshots: vite: 8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.0) vue: 3.5.39(typescript@6.0.2) - '@vitest/browser-playwright@4.1.10(bufferutil@4.1.0)(msw@2.15.0(@types/node@26.1.1)(@typescript/typescript6@6.0.2))(playwright@1.61.1)(utf-8-validate@6.0.6)(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.0))(vitest@4.1.10)': + '@vitest/browser-playwright@4.1.10(bufferutil@4.1.0)(msw@2.15.0(@types/node@26.1.1)(@typescript/typescript6@6.0.2))(playwright@1.61.1)(utf-8-validate@6.0.6)(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.1))(vitest@4.1.10)': dependencies: - '@vitest/browser': 4.1.10(bufferutil@4.1.0)(msw@2.15.0(@types/node@26.1.1)(@typescript/typescript6@6.0.2))(utf-8-validate@6.0.6)(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.0))(vitest@4.1.10) - '@vitest/mocker': 4.1.10(msw@2.15.0(@types/node@26.1.1)(@typescript/typescript6@6.0.2))(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.0)) + '@vitest/browser': 4.1.10(bufferutil@4.1.0)(msw@2.15.0(@types/node@26.1.1)(@typescript/typescript6@6.0.2))(utf-8-validate@6.0.6)(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.1))(vitest@4.1.10) + '@vitest/mocker': 4.1.10(msw@2.15.0(@types/node@26.1.1)(@typescript/typescript6@6.0.2))(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.1)) playwright: 1.61.1 tinyrainbow: 3.1.0 - vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.1.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(happy-dom@20.10.6(bufferutil@4.1.0)(utf-8-validate@6.0.6))(msw@2.15.0(@types/node@26.1.1)(@typescript/typescript6@6.0.2))(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.0)) + vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.1.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(happy-dom@20.10.6(bufferutil@4.1.0)(utf-8-validate@6.0.6))(msw@2.15.0(@types/node@26.1.1)(@typescript/typescript6@6.0.2))(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.1)) + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite + optional: true + + '@vitest/browser-playwright@4.1.10(bufferutil@4.1.0)(msw@2.15.0(@types/node@26.1.1)(typescript@5.9.3))(playwright@1.61.1)(utf-8-validate@6.0.6)(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.1))(vitest@4.1.10)': + dependencies: + '@vitest/browser': 4.1.10(bufferutil@4.1.0)(msw@2.15.0(@types/node@26.1.1)(typescript@5.9.3))(utf-8-validate@6.0.6)(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.1))(vitest@4.1.10) + '@vitest/mocker': 4.1.10(msw@2.15.0(@types/node@26.1.1)(typescript@5.9.3))(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.1)) + playwright: 1.61.1 + tinyrainbow: 3.1.0 + vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.1.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(happy-dom@20.10.6(bufferutil@4.1.0)(utf-8-validate@6.0.6))(msw@2.15.0(@types/node@26.1.1)(typescript@5.9.3))(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.1)) transitivePeerDependencies: - bufferutil - msw @@ -12266,16 +12408,48 @@ snapshots: - utf-8-validate - vite - '@vitest/browser@4.1.10(bufferutil@4.1.0)(msw@2.15.0(@types/node@26.1.1)(@typescript/typescript6@6.0.2))(utf-8-validate@6.0.6)(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.0))(vitest@4.1.10)': + '@vitest/browser-playwright@4.1.10(bufferutil@4.1.0)(msw@2.15.0(@types/node@26.1.1)(typescript@7.0.2))(playwright@1.61.1)(utf-8-validate@6.0.6)(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.1))(vitest@4.1.10)': + dependencies: + '@vitest/browser': 4.1.10(bufferutil@4.1.0)(msw@2.15.0(@types/node@26.1.1)(typescript@7.0.2))(utf-8-validate@6.0.6)(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.1))(vitest@4.1.10) + '@vitest/mocker': 4.1.10(msw@2.15.0(@types/node@26.1.1)(typescript@7.0.2))(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.1)) + playwright: 1.61.1 + tinyrainbow: 3.1.0 + vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.1.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(happy-dom@20.10.6(bufferutil@4.1.0)(utf-8-validate@6.0.6))(msw@2.15.0(@types/node@26.1.1)(typescript@7.0.2))(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.1)) + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite + optional: true + + '@vitest/browser@4.1.10(bufferutil@4.1.0)(msw@2.15.0(@types/node@26.1.1)(@typescript/typescript6@6.0.2))(utf-8-validate@6.0.6)(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.1))(vitest@4.1.10)': dependencies: '@blazediff/core': 1.9.1 - '@vitest/mocker': 4.1.10(msw@2.15.0(@types/node@26.1.1)(@typescript/typescript6@6.0.2))(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.0)) + '@vitest/mocker': 4.1.10(msw@2.15.0(@types/node@26.1.1)(@typescript/typescript6@6.0.2))(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.1)) '@vitest/utils': 4.1.10 magic-string: 0.30.21 pngjs: 7.0.0 sirv: 3.0.2 tinyrainbow: 3.1.0 - vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.1.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(happy-dom@20.10.6(bufferutil@4.1.0)(utf-8-validate@6.0.6))(msw@2.15.0(@types/node@26.1.1)(@typescript/typescript6@6.0.2))(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.0)) + vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.1.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(happy-dom@20.10.6(bufferutil@4.1.0)(utf-8-validate@6.0.6))(msw@2.15.0(@types/node@26.1.1)(@typescript/typescript6@6.0.2))(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.1)) + ws: 8.21.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite + optional: true + + '@vitest/browser@4.1.10(bufferutil@4.1.0)(msw@2.15.0(@types/node@26.1.1)(typescript@5.9.3))(utf-8-validate@6.0.6)(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.1))(vitest@4.1.10)': + dependencies: + '@blazediff/core': 1.9.1 + '@vitest/mocker': 4.1.10(msw@2.15.0(@types/node@26.1.1)(typescript@5.9.3))(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.1)) + '@vitest/utils': 4.1.10 + magic-string: 0.30.21 + pngjs: 7.0.0 + sirv: 3.0.2 + tinyrainbow: 3.1.0 + vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.1.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(happy-dom@20.10.6(bufferutil@4.1.0)(utf-8-validate@6.0.6))(msw@2.15.0(@types/node@26.1.1)(typescript@5.9.3))(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.1)) ws: 8.21.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) transitivePeerDependencies: - bufferutil @@ -12301,6 +12475,24 @@ snapshots: - utf-8-validate - vite + '@vitest/browser@4.1.10(bufferutil@4.1.0)(msw@2.15.0(@types/node@26.1.1)(typescript@7.0.2))(utf-8-validate@6.0.6)(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.1))(vitest@4.1.10)': + dependencies: + '@blazediff/core': 1.9.1 + '@vitest/mocker': 4.1.10(msw@2.15.0(@types/node@26.1.1)(typescript@7.0.2))(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.1)) + '@vitest/utils': 4.1.10 + magic-string: 0.30.21 + pngjs: 7.0.0 + sirv: 3.0.2 + tinyrainbow: 3.1.0 + vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.1.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(happy-dom@20.10.6(bufferutil@4.1.0)(utf-8-validate@6.0.6))(msw@2.15.0(@types/node@26.1.1)(typescript@7.0.2))(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.1)) + ws: 8.21.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite + optional: true + '@vitest/coverage-v8@4.1.10(@vitest/browser@4.1.10)(vitest@4.1.10)': dependencies: '@bcoe/v8-coverage': 1.0.2 @@ -12313,9 +12505,9 @@ snapshots: obug: 2.1.1 std-env: 4.1.0 tinyrainbow: 3.1.0 - vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.1.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(happy-dom@20.10.6(bufferutil@4.1.0)(utf-8-validate@6.0.6))(msw@2.15.0(@types/node@26.1.1)(@typescript/typescript6@6.0.2))(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.0)) + vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.1.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(happy-dom@20.10.6(bufferutil@4.1.0)(utf-8-validate@6.0.6))(msw@2.15.0(@types/node@26.1.1)(typescript@7.0.2))(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.1)) optionalDependencies: - '@vitest/browser': 4.1.10(bufferutil@4.1.0)(msw@2.15.0(@types/node@26.1.1)(@typescript/typescript6@6.0.2))(utf-8-validate@6.0.6)(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.0))(vitest@4.1.10) + '@vitest/browser': 4.1.10(bufferutil@4.1.0)(msw@2.15.0(@types/node@26.1.1)(typescript@7.0.2))(utf-8-validate@6.0.6)(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.1))(vitest@4.1.10) '@vitest/expect@2.0.5': dependencies: @@ -12341,14 +12533,23 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.10(msw@2.15.0(@types/node@26.1.1)(@typescript/typescript6@6.0.2))(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.0))': + '@vitest/mocker@4.1.10(msw@2.15.0(@types/node@26.1.1)(@typescript/typescript6@6.0.2))(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.1))': dependencies: '@vitest/spy': 4.1.10 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: msw: 2.15.0(@types/node@26.1.1)(@typescript/typescript6@6.0.2) - vite: 8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.0) + vite: 8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.1) + + '@vitest/mocker@4.1.10(msw@2.15.0(@types/node@26.1.1)(typescript@5.9.3))(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.1))': + dependencies: + '@vitest/spy': 4.1.10 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + msw: 2.15.0(@types/node@26.1.1)(typescript@5.9.3) + vite: 8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.1) '@vitest/mocker@4.1.10(msw@2.15.0(@types/node@26.1.1)(typescript@6.0.2))(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.0))': dependencies: @@ -12359,6 +12560,15 @@ snapshots: msw: 2.15.0(@types/node@26.1.1)(typescript@6.0.2) vite: 8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.0) + '@vitest/mocker@4.1.10(msw@2.15.0(@types/node@26.1.1)(typescript@7.0.2))(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.1))': + dependencies: + '@vitest/spy': 4.1.10 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + msw: 2.15.0(@types/node@26.1.1)(typescript@7.0.2) + vite: 8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.1) + '@vitest/pretty-format@2.0.5': dependencies: tinyrainbow: 1.2.0 @@ -15563,6 +15773,32 @@ snapshots: - '@types/node' optional: true + msw@2.15.0(@types/node@26.1.1)(typescript@5.9.3): + dependencies: + '@inquirer/confirm': 6.0.12(@types/node@26.1.1) + '@mswjs/interceptors': 0.41.8 + '@open-draft/deferred-promise': 3.0.0 + '@types/statuses': 2.0.6 + cookie: 1.1.1 + graphql: 16.14.0 + headers-polyfill: 5.0.1 + is-node-process: 1.2.0 + outvariant: 1.4.3 + path-to-regexp: 6.3.0 + picocolors: 1.1.1 + rettime: 0.11.11 + statuses: 2.0.2 + strict-event-emitter: 0.5.1 + tough-cookie: 6.0.1 + type-fest: 5.6.0 + until-async: 3.0.2 + yargs: 17.7.2 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - '@types/node' + optional: true + msw@2.15.0(@types/node@26.1.1)(typescript@6.0.2): dependencies: '@inquirer/confirm': 6.0.12(@types/node@26.1.1) @@ -15588,6 +15824,32 @@ snapshots: transitivePeerDependencies: - '@types/node' + msw@2.15.0(@types/node@26.1.1)(typescript@7.0.2): + dependencies: + '@inquirer/confirm': 6.0.12(@types/node@26.1.1) + '@mswjs/interceptors': 0.41.8 + '@open-draft/deferred-promise': 3.0.0 + '@types/statuses': 2.0.6 + cookie: 1.1.1 + graphql: 16.14.0 + headers-polyfill: 5.0.1 + is-node-process: 1.2.0 + outvariant: 1.4.3 + path-to-regexp: 6.3.0 + picocolors: 1.1.1 + rettime: 0.11.11 + statuses: 2.0.2 + strict-event-emitter: 0.5.1 + tough-cookie: 6.0.1 + type-fest: 5.6.0 + until-async: 3.0.2 + yargs: 17.7.2 + optionalDependencies: + typescript: 7.0.2 + transitivePeerDependencies: + - '@types/node' + optional: true + muggle-string@0.4.1: {} multer@2.2.0: @@ -16494,6 +16756,15 @@ snapshots: mdast-util-to-markdown: 2.1.2 unified: 11.0.5 + remark@15.0.1: + dependencies: + '@types/mdast': 4.0.4 + remark-parse: 11.0.0 + remark-stringify: 11.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + rename@1.0.4: dependencies: debug: 2.6.9 @@ -17482,6 +17753,12 @@ snapshots: optionalDependencies: fsevents: 2.3.3 + tsx@4.23.1: + dependencies: + esbuild: 0.28.1 + optionalDependencies: + fsevents: 2.3.3 + tsyringe@4.10.0: dependencies: tslib: 1.14.1 @@ -17773,6 +18050,22 @@ snapshots: terser: 5.48.0 tsx: 4.23.0 + vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.1): + dependencies: + lightningcss: 1.32.0 + picomatch: 4.0.5 + postcss: 8.5.18 + rolldown: 1.1.5 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 26.1.1 + esbuild: 0.28.1 + fsevents: 2.3.3 + sass: 1.100.0 + sass-embedded: 1.100.0 + terser: 5.48.0 + tsx: 4.23.1 + vitest-fetch-mock@0.4.5(vitest@4.1.10): dependencies: vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.1.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(happy-dom@20.10.6(bufferutil@4.1.0)(utf-8-validate@6.0.6))(msw@2.15.0(@types/node@26.1.1)(typescript@6.0.2))(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.0)) @@ -17781,17 +18074,17 @@ snapshots: dependencies: ts-essentials: 10.2.0(@typescript/typescript6@6.0.2) typescript: '@typescript/typescript6@6.0.2' - vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.1.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(happy-dom@20.10.6(bufferutil@4.1.0)(utf-8-validate@6.0.6))(msw@2.15.0(@types/node@26.1.1)(@typescript/typescript6@6.0.2))(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.0)) + vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.1.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(happy-dom@20.10.6(bufferutil@4.1.0)(utf-8-validate@6.0.6))(msw@2.15.0(@types/node@26.1.1)(@typescript/typescript6@6.0.2))(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.1)) vitest-websocket-mock@0.7.0(vitest@4.1.10): dependencies: mock-socket: 9.3.1 - vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.1.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(happy-dom@20.10.6(bufferutil@4.1.0)(utf-8-validate@6.0.6))(msw@2.15.0(@types/node@26.1.1)(@typescript/typescript6@6.0.2))(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.0)) + vitest: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.1.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(happy-dom@20.10.6(bufferutil@4.1.0)(utf-8-validate@6.0.6))(msw@2.15.0(@types/node@26.1.1)(@typescript/typescript6@6.0.2))(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.1)) - vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.1.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(happy-dom@20.10.6(bufferutil@4.1.0)(utf-8-validate@6.0.6))(msw@2.15.0(@types/node@26.1.1)(@typescript/typescript6@6.0.2))(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.0)): + vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.1.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(happy-dom@20.10.6(bufferutil@4.1.0)(utf-8-validate@6.0.6))(msw@2.15.0(@types/node@26.1.1)(@typescript/typescript6@6.0.2))(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.1)): dependencies: '@vitest/expect': 4.1.10 - '@vitest/mocker': 4.1.10(msw@2.15.0(@types/node@26.1.1)(@typescript/typescript6@6.0.2))(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.0)) + '@vitest/mocker': 4.1.10(msw@2.15.0(@types/node@26.1.1)(@typescript/typescript6@6.0.2))(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.1)) '@vitest/pretty-format': 4.1.10 '@vitest/runner': 4.1.10 '@vitest/snapshot': 4.1.10 @@ -17808,12 +18101,43 @@ snapshots: tinyexec: 1.1.2 tinyglobby: 0.2.17 tinyrainbow: 3.1.0 - vite: 8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.0) + vite: 8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.1) why-is-node-running: 2.3.0 optionalDependencies: '@opentelemetry/api': 1.9.1 '@types/node': 26.1.1 - '@vitest/browser-playwright': 4.1.10(bufferutil@4.1.0)(msw@2.15.0(@types/node@26.1.1)(@typescript/typescript6@6.0.2))(playwright@1.61.1)(utf-8-validate@6.0.6)(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.0))(vitest@4.1.10) + '@vitest/browser-playwright': 4.1.10(bufferutil@4.1.0)(msw@2.15.0(@types/node@26.1.1)(@typescript/typescript6@6.0.2))(playwright@1.61.1)(utf-8-validate@6.0.6)(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.1))(vitest@4.1.10) + '@vitest/coverage-v8': 4.1.10(@vitest/browser@4.1.10)(vitest@4.1.10) + happy-dom: 20.10.6(bufferutil@4.1.0)(utf-8-validate@6.0.6) + transitivePeerDependencies: + - msw + + vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.1.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(happy-dom@20.10.6(bufferutil@4.1.0)(utf-8-validate@6.0.6))(msw@2.15.0(@types/node@26.1.1)(typescript@5.9.3))(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.1)): + dependencies: + '@vitest/expect': 4.1.10 + '@vitest/mocker': 4.1.10(msw@2.15.0(@types/node@26.1.1)(typescript@5.9.3))(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.1)) + '@vitest/pretty-format': 4.1.10 + '@vitest/runner': 4.1.10 + '@vitest/snapshot': 4.1.10 + '@vitest/spy': 4.1.10 + '@vitest/utils': 4.1.10 + es-module-lexer: 2.1.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.5 + std-env: 4.1.0 + tinybench: 2.9.0 + tinyexec: 1.1.2 + tinyglobby: 0.2.17 + tinyrainbow: 3.1.0 + vite: 8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.1) + why-is-node-running: 2.3.0 + optionalDependencies: + '@opentelemetry/api': 1.9.1 + '@types/node': 26.1.1 + '@vitest/browser-playwright': 4.1.10(bufferutil@4.1.0)(msw@2.15.0(@types/node@26.1.1)(typescript@5.9.3))(playwright@1.61.1)(utf-8-validate@6.0.6)(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.1))(vitest@4.1.10) '@vitest/coverage-v8': 4.1.10(@vitest/browser@4.1.10)(vitest@4.1.10) happy-dom: 20.10.6(bufferutil@4.1.0)(utf-8-validate@6.0.6) transitivePeerDependencies: @@ -17850,6 +18174,37 @@ snapshots: transitivePeerDependencies: - msw + vitest@4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.1.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(happy-dom@20.10.6(bufferutil@4.1.0)(utf-8-validate@6.0.6))(msw@2.15.0(@types/node@26.1.1)(typescript@7.0.2))(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.1)): + dependencies: + '@vitest/expect': 4.1.10 + '@vitest/mocker': 4.1.10(msw@2.15.0(@types/node@26.1.1)(typescript@7.0.2))(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.1)) + '@vitest/pretty-format': 4.1.10 + '@vitest/runner': 4.1.10 + '@vitest/snapshot': 4.1.10 + '@vitest/spy': 4.1.10 + '@vitest/utils': 4.1.10 + es-module-lexer: 2.1.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.5 + std-env: 4.1.0 + tinybench: 2.9.0 + tinyexec: 1.1.2 + tinyglobby: 0.2.17 + tinyrainbow: 3.1.0 + vite: 8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.1) + why-is-node-running: 2.3.0 + optionalDependencies: + '@opentelemetry/api': 1.9.1 + '@types/node': 26.1.1 + '@vitest/browser-playwright': 4.1.10(bufferutil@4.1.0)(msw@2.15.0(@types/node@26.1.1)(typescript@7.0.2))(playwright@1.61.1)(utf-8-validate@6.0.6)(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.1))(vitest@4.1.10) + '@vitest/coverage-v8': 4.1.10(@vitest/browser@4.1.10)(vitest@4.1.10) + happy-dom: 20.10.6(bufferutil@4.1.0)(utf-8-validate@6.0.6) + transitivePeerDependencies: + - msw + void-elements@3.1.0: {} vscode-jsonrpc@8.2.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index b6b2b67cf5..c280df00f2 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -11,6 +11,7 @@ packages: - packages/misskey-js/generator - packages/misskey-reversi - packages/misskey-bubble-game + - packages-private/* allowBuilds: '@nestjs/core': true '@parcel/watcher': true diff --git a/scripts/changelog-checker/.gitignore b/scripts/changelog-checker/.gitignore deleted file mode 100644 index 882936f9bd..0000000000 --- a/scripts/changelog-checker/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -node_modules -coverage -.idea \ No newline at end of file diff --git a/scripts/changelog-checker/package-lock.json b/scripts/changelog-checker/package-lock.json deleted file mode 100644 index f2f48d92f7..0000000000 --- a/scripts/changelog-checker/package-lock.json +++ /dev/null @@ -1,2773 +0,0 @@ -{ - "name": "changelog-checker", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "changelog-checker", - "version": "1.0.0", - "devDependencies": { - "@types/mdast": "4.0.4", - "@types/node": "26.1.1", - "@vitest/coverage-v8": "4.1.10", - "mdast-util-to-string": "4.0.0", - "remark": "15.0.1", - "remark-parse": "11.0.0", - "typescript": "7.0.2", - "unified": "11.0.5", - "vite": "8.1.4", - "vite-node": "6.0.0", - "vitest": "4.1.10" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", - "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.29.0" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@bcoe/v8-coverage": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", - "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/@emnapi/core": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", - "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.2", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", - "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", - "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", - "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@tybys/wasm-util": "^0.10.3" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - }, - "peerDependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1" - } - }, - "node_modules/@oxc-project/types": { - "version": "0.139.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz", - "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/Boshen" - } - }, - "node_modules/@rolldown/binding-android-arm64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz", - "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz", - "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz", - "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz", - "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz", - "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz", - "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz", - "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz", - "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz", - "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz", - "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz", - "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz", - "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz", - "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==", - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/core": "1.11.1", - "@emnapi/runtime": "1.11.1", - "@napi-rs/wasm-runtime": "^1.1.6" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz", - "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz", - "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", - "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@standard-schema/spec": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", - "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", - "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@types/chai": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", - "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/deep-eql": "*", - "assertion-error": "^2.0.1" - } - }, - "node_modules/@types/debug": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", - "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", - "dev": true, - "dependencies": { - "@types/ms": "*" - } - }, - "node_modules/@types/deep-eql": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", - "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/mdast": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", - "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/unist": "*" - } - }, - "node_modules/@types/ms": { - "version": "0.7.34", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.34.tgz", - "integrity": "sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==", - "dev": true - }, - "node_modules/@types/node": { - "version": "26.1.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz", - "integrity": "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==", - "dev": true, - "license": "MIT", - "dependencies": { - "undici-types": "~8.3.0" - } - }, - "node_modules/@types/unist": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.2.tgz", - "integrity": "sha512-dqId9J8K/vGi5Zr7oo212BGii5m3q5Hxlkwy3WpYuKPklmBEvsbMYYyLxAQpSffdLl/gdW0XUpKWFvYmyoWCoQ==", - "dev": true - }, - "node_modules/@typescript/typescript-aix-ppc64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz", - "integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-darwin-arm64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz", - "integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-darwin-x64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz", - "integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-freebsd-arm64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz", - "integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-freebsd-x64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz", - "integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-linux-arm": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz", - "integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-linux-arm64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz", - "integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-linux-loong64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz", - "integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-linux-mips64el": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz", - "integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-linux-ppc64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz", - "integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-linux-riscv64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz", - "integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-linux-s390x": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz", - "integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-linux-x64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz", - "integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-netbsd-arm64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz", - "integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-netbsd-x64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz", - "integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-openbsd-arm64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz", - "integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-openbsd-x64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz", - "integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-sunos-x64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz", - "integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-win32-arm64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz", - "integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@typescript/typescript-win32-x64": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz", - "integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=16.20.0" - } - }, - "node_modules/@vitest/coverage-v8": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.10.tgz", - "integrity": "sha512-IM49HmthevbgAO4anp1hwtoT9wYe59w0LR00gr+eagHE+ZJ5lK4sLPeO0ubgoJcwLk6dehU3R24N+FbEEKDc8g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@bcoe/v8-coverage": "^1.0.2", - "@vitest/utils": "4.1.10", - "ast-v8-to-istanbul": "^1.0.0", - "istanbul-lib-coverage": "^3.2.2", - "istanbul-lib-report": "^3.0.1", - "istanbul-reports": "^3.2.0", - "magicast": "^0.5.2", - "obug": "^2.1.1", - "std-env": "^4.0.0-rc.1", - "tinyrainbow": "^3.1.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@vitest/browser": "4.1.10", - "vitest": "4.1.10" - }, - "peerDependenciesMeta": { - "@vitest/browser": { - "optional": true - } - } - }, - "node_modules/@vitest/expect": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", - "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@standard-schema/spec": "^1.1.0", - "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.10", - "@vitest/utils": "4.1.10", - "chai": "^6.2.2", - "tinyrainbow": "^3.1.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/mocker": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", - "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/spy": "4.1.10", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.21" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "msw": "^2.4.9", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } - } - }, - "node_modules/@vitest/pretty-format": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", - "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "tinyrainbow": "^3.1.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/runner": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz", - "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/utils": "4.1.10", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/snapshot": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz", - "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "4.1.10", - "@vitest/utils": "4.1.10", - "magic-string": "^0.30.21", - "pathe": "^2.0.3" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/spy": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz", - "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/@vitest/utils": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz", - "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/pretty-format": "4.1.10", - "convert-source-map": "^2.0.0", - "tinyrainbow": "^3.1.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/assertion-error": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - } - }, - "node_modules/ast-v8-to-istanbul": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.0.tgz", - "integrity": "sha512-1fSfIwuDICFA4LKkCzRPO7F0hzFf0B7+Xqrl27ynQaa+Rh0e1Es0v6kWHPott3lU10AyAr7oKHa65OppjLn3Rg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.31", - "estree-walker": "^3.0.3", - "js-tokens": "^10.0.0" - } - }, - "node_modules/bail": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", - "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", - "dev": true, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/cac": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/cac/-/cac-7.0.0.tgz", - "integrity": "sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20.19.0" - } - }, - "node_modules/chai": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", - "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/character-entities": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", - "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", - "dev": true, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "license": "MIT" - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decode-named-character-reference": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz", - "integrity": "sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==", - "dev": true, - "dependencies": { - "character-entities": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/dequal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, - "node_modules/devlop": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", - "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", - "dev": true, - "dependencies": { - "dequal": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/es-module-lexer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", - "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", - "dev": true, - "license": "MIT" - }, - "node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0" - } - }, - "node_modules/expect-type": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", - "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "dev": true - }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true - }, - "node_modules/is-plain-obj": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/istanbul-lib-coverage": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", - "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/istanbul-lib-report": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", - "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", - "dev": true, - "dependencies": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^4.0.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/istanbul-reports": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", - "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/js-tokens": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", - "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/lightningcss": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", - "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", - "dev": true, - "license": "MPL-2.0", - "dependencies": { - "detect-libc": "^2.0.3" - }, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "lightningcss-android-arm64": "1.32.0", - "lightningcss-darwin-arm64": "1.32.0", - "lightningcss-darwin-x64": "1.32.0", - "lightningcss-freebsd-x64": "1.32.0", - "lightningcss-linux-arm-gnueabihf": "1.32.0", - "lightningcss-linux-arm64-gnu": "1.32.0", - "lightningcss-linux-arm64-musl": "1.32.0", - "lightningcss-linux-x64-gnu": "1.32.0", - "lightningcss-linux-x64-musl": "1.32.0", - "lightningcss-win32-arm64-msvc": "1.32.0", - "lightningcss-win32-x64-msvc": "1.32.0" - } - }, - "node_modules/lightningcss-android-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", - "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", - "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", - "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-freebsd-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", - "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", - "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", - "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", - "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", - "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", - "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", - "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", - "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/longest-streak": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", - "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", - "dev": true, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" - } - }, - "node_modules/magicast": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.2.tgz", - "integrity": "sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.29.0", - "@babel/types": "^7.29.0", - "source-map-js": "^1.2.1" - } - }, - "node_modules/make-dir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", - "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", - "dev": true, - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mdast-util-from-markdown": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.0.tgz", - "integrity": "sha512-n7MTOr/z+8NAX/wmhhDji8O3bRvPTV/U0oTCaZJkjhPSKTPhS3xufVhKGF8s1pJ7Ox4QgoIU7KHseh09S+9rTA==", - "dev": true, - "dependencies": { - "@types/mdast": "^4.0.0", - "@types/unist": "^3.0.0", - "decode-named-character-reference": "^1.0.0", - "devlop": "^1.0.0", - "mdast-util-to-string": "^4.0.0", - "micromark": "^4.0.0", - "micromark-util-decode-numeric-character-reference": "^2.0.0", - "micromark-util-decode-string": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0", - "unist-util-stringify-position": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-phrasing": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.0.0.tgz", - "integrity": "sha512-xadSsJayQIucJ9n053dfQwVu1kuXg7jCTdYsMK8rqzKZh52nLfSH/k0sAxE0u+pj/zKZX+o5wB+ML5mRayOxFA==", - "dev": true, - "dependencies": { - "@types/mdast": "^4.0.0", - "unist-util-is": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-markdown": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.0.tgz", - "integrity": "sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ==", - "dev": true, - "dependencies": { - "@types/mdast": "^4.0.0", - "@types/unist": "^3.0.0", - "longest-streak": "^3.0.0", - "mdast-util-phrasing": "^4.0.0", - "mdast-util-to-string": "^4.0.0", - "micromark-util-decode-string": "^2.0.0", - "unist-util-visit": "^5.0.0", - "zwitch": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", - "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", - "dev": true, - "dependencies": { - "@types/mdast": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.0.tgz", - "integrity": "sha512-o/sd0nMof8kYff+TqcDx3VSrgBTcZpSvYcAHIfHhv5VAuNmisCxjhx6YmxS8PFEpb9z5WKWKPdzf0jM23ro3RQ==", - "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "@types/debug": "^4.0.0", - "debug": "^4.0.0", - "decode-named-character-reference": "^1.0.0", - "devlop": "^1.0.0", - "micromark-core-commonmark": "^2.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-combine-extensions": "^2.0.0", - "micromark-util-decode-numeric-character-reference": "^2.0.0", - "micromark-util-encode": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-resolve-all": "^2.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "micromark-util-subtokenize": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-core-commonmark": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.0.tgz", - "integrity": "sha512-jThOz/pVmAYUtkroV3D5c1osFXAMv9e0ypGDOIZuCeAe91/sD6BoE2Sjzt30yuXtwOYUmySOhMas/PVyh02itA==", - "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "decode-named-character-reference": "^1.0.0", - "devlop": "^1.0.0", - "micromark-factory-destination": "^2.0.0", - "micromark-factory-label": "^2.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-factory-title": "^2.0.0", - "micromark-factory-whitespace": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-classify-character": "^2.0.0", - "micromark-util-html-tag-name": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-resolve-all": "^2.0.0", - "micromark-util-subtokenize": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-destination": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.0.tgz", - "integrity": "sha512-j9DGrQLm/Uhl2tCzcbLhy5kXsgkHUrjJHg4fFAeoMRwJmJerT9aw4FEhIbZStWN8A3qMwOp1uzHr4UL8AInxtA==", - "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-label": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.0.tgz", - "integrity": "sha512-RR3i96ohZGde//4WSe/dJsxOX6vxIg9TimLAS3i4EhBAFx8Sm5SmqVfR8E87DPSR31nEAjZfbt91OMZWcNgdZw==", - "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "devlop": "^1.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-space": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.0.tgz", - "integrity": "sha512-TKr+LIDX2pkBJXFLzpyPyljzYK3MtmllMUMODTQJIUfDGncESaqB90db9IAUcz4AZAJFdd8U9zOp9ty1458rxg==", - "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-title": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.0.tgz", - "integrity": "sha512-jY8CSxmpWLOxS+t8W+FG3Xigc0RDQA9bKMY/EwILvsesiRniiVMejYTE4wumNc2f4UbAa4WsHqe3J1QS1sli+A==", - "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-whitespace": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.0.tgz", - "integrity": "sha512-28kbwaBjc5yAI1XadbdPYHX/eDnqaUFVikLwrO7FDnKG7lpgxnvk/XGRhX/PN0mOZ+dBSZ+LgunHS+6tYQAzhA==", - "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-character": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.0.1.tgz", - "integrity": "sha512-3wgnrmEAJ4T+mGXAUfMvMAbxU9RDG43XmGce4j6CwPtVxB3vfwXSZ6KhFwDzZ3mZHhmPimMAXg71veiBGzeAZw==", - "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-chunked": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.0.tgz", - "integrity": "sha512-anK8SWmNphkXdaKgz5hJvGa7l00qmcaUQoMYsBwDlSKFKjc6gjGXPDw3FNL3Nbwq5L8gE+RCbGqTw49FK5Qyvg==", - "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-classify-character": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.0.tgz", - "integrity": "sha512-S0ze2R9GH+fu41FA7pbSqNWObo/kzwf8rN/+IGlW/4tC6oACOs8B++bh+i9bVyNnwCcuksbFwsBme5OCKXCwIw==", - "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-combine-extensions": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.0.tgz", - "integrity": "sha512-vZZio48k7ON0fVS3CUgFatWHoKbbLTK/rT7pzpJ4Bjp5JjkZeasRfrS9wsBdDJK2cJLHMckXZdzPSSr1B8a4oQ==", - "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-chunked": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-decode-numeric-character-reference": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.1.tgz", - "integrity": "sha512-bmkNc7z8Wn6kgjZmVHOX3SowGmVdhYS7yBpMnuMnPzDq/6xwVA604DuOXMZTO1lvq01g+Adfa0pE2UKGlxL1XQ==", - "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-decode-string": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.0.tgz", - "integrity": "sha512-r4Sc6leeUTn3P6gk20aFMj2ntPwn6qpDZqWvYmAG6NgvFTIlj4WtrAudLi65qYoaGdXYViXYw2pkmn7QnIFasA==", - "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "decode-named-character-reference": "^1.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-decode-numeric-character-reference": "^2.0.0", - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-encode": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.0.tgz", - "integrity": "sha512-pS+ROfCXAGLWCOc8egcBvT0kf27GoWMqtdarNfDcjb6YLuV5cM3ioG45Ys2qOVqeqSbjaKg72vU+Wby3eddPsA==", - "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ] - }, - "node_modules/micromark-util-html-tag-name": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.0.tgz", - "integrity": "sha512-xNn4Pqkj2puRhKdKTm8t1YHC/BAjx6CEwRFXntTaRf/x16aqka6ouVoutm+QdkISTlT7e2zU7U4ZdlDLJd2Mcw==", - "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ] - }, - "node_modules/micromark-util-normalize-identifier": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.0.tgz", - "integrity": "sha512-2xhYT0sfo85FMrUPtHcPo2rrp1lwbDEEzpx7jiH2xXJLqBuy4H0GgXk5ToU8IEwoROtXuL8ND0ttVa4rNqYK3w==", - "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-resolve-all": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.0.tgz", - "integrity": "sha512-6KU6qO7DZ7GJkaCgwBNtplXCvGkJToU86ybBAUdavvgsCiG8lSSvYxr9MhwmQ+udpzywHsl4RpGJsYWG1pDOcA==", - "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-sanitize-uri": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.0.tgz", - "integrity": "sha512-WhYv5UEcZrbAtlsnPuChHUAsu/iBPOVaEVsntLBIdpibO0ddy8OzavZz3iL2xVvBZOpolujSliP65Kq0/7KIYw==", - "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-encode": "^2.0.0", - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-subtokenize": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.0.0.tgz", - "integrity": "sha512-vc93L1t+gpR3p8jxeVdaYlbV2jTYteDje19rNSS/H5dlhxUYll5Fy6vJ2cDwP8RnsXi818yGty1ayP55y3W6fg==", - "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "dependencies": { - "devlop": "^1.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-symbol": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.0.tgz", - "integrity": "sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==", - "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ] - }, - "node_modules/micromark-util-types": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.0.tgz", - "integrity": "sha512-oNh6S2WMHWRZrmutsRmDDfkzKtxF+bc2VxLC9dvtrDIRFln627VsFP6fLMgTryGDljgLPjkrzQSDcPrjPyDJ5w==", - "dev": true, - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ] - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/nanoid": { - "version": "3.3.16", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", - "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/obug": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", - "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", - "dev": true, - "funding": [ - "https://github.com/sponsors/sxzz", - "https://opencollective.com/debug" - ], - "license": "MIT" - }, - "node_modules/pathe": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", - "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", - "dev": true, - "license": "MIT" - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", - "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/postcss": { - "version": "8.5.20", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.20.tgz", - "integrity": "sha512-lW616l85ucIQL+FocMmL7pQFPqBmwejrCMg+iPxyImlrANNJG9NHq/RkyCZopDhd8C3LA03PHRJDjkbGu8vvug==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.16", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/remark": { - "version": "15.0.1", - "resolved": "https://registry.npmjs.org/remark/-/remark-15.0.1.tgz", - "integrity": "sha512-Eht5w30ruCXgFmxVUSlNWQ9iiimq07URKeFS3hNc8cUWy1llX4KDWfyEDZRycMc+znsN9Ux5/tJ/BFdgdOwA3A==", - "dev": true, - "dependencies": { - "@types/mdast": "^4.0.0", - "remark-parse": "^11.0.0", - "remark-stringify": "^11.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-parse": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", - "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", - "dev": true, - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-from-markdown": "^2.0.0", - "micromark-util-types": "^2.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-stringify": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", - "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", - "dev": true, - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-to-markdown": "^2.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/rolldown": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz", - "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@oxc-project/types": "=0.139.0", - "@rolldown/pluginutils": "^1.0.0" - }, - "bin": { - "rolldown": "bin/cli.mjs" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.1.5", - "@rolldown/binding-darwin-arm64": "1.1.5", - "@rolldown/binding-darwin-x64": "1.1.5", - "@rolldown/binding-freebsd-x64": "1.1.5", - "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", - "@rolldown/binding-linux-arm64-gnu": "1.1.5", - "@rolldown/binding-linux-arm64-musl": "1.1.5", - "@rolldown/binding-linux-ppc64-gnu": "1.1.5", - "@rolldown/binding-linux-s390x-gnu": "1.1.5", - "@rolldown/binding-linux-x64-gnu": "1.1.5", - "@rolldown/binding-linux-x64-musl": "1.1.5", - "@rolldown/binding-openharmony-arm64": "1.1.5", - "@rolldown/binding-wasm32-wasi": "1.1.5", - "@rolldown/binding-win32-arm64-msvc": "1.1.5", - "@rolldown/binding-win32-x64-msvc": "1.1.5" - } - }, - "node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/siginfo": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", - "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", - "dev": true, - "license": "ISC" - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/stackback": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", - "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", - "dev": true, - "license": "MIT" - }, - "node_modules/std-env": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", - "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", - "dev": true, - "license": "MIT" - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/tinybench": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", - "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", - "dev": true, - "license": "MIT" - }, - "node_modules/tinyexec": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", - "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/tinyglobby": { - "version": "0.2.17", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", - "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.4" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/tinyrainbow": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", - "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/trough": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/trough/-/trough-2.1.0.tgz", - "integrity": "sha512-AqTiAOLcj85xS7vQ8QkAV41hPDIJ71XJB4RCUrzo/1GM2CQwhkJGaf9Hgr7BOugMRpgGUrqRg/DrBDl4H40+8g==", - "dev": true, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD", - "optional": true - }, - "node_modules/typescript": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz", - "integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc" - }, - "engines": { - "node": ">=16.20.0" - }, - "optionalDependencies": { - "@typescript/typescript-aix-ppc64": "7.0.2", - "@typescript/typescript-darwin-arm64": "7.0.2", - "@typescript/typescript-darwin-x64": "7.0.2", - "@typescript/typescript-freebsd-arm64": "7.0.2", - "@typescript/typescript-freebsd-x64": "7.0.2", - "@typescript/typescript-linux-arm": "7.0.2", - "@typescript/typescript-linux-arm64": "7.0.2", - "@typescript/typescript-linux-loong64": "7.0.2", - "@typescript/typescript-linux-mips64el": "7.0.2", - "@typescript/typescript-linux-ppc64": "7.0.2", - "@typescript/typescript-linux-riscv64": "7.0.2", - "@typescript/typescript-linux-s390x": "7.0.2", - "@typescript/typescript-linux-x64": "7.0.2", - "@typescript/typescript-netbsd-arm64": "7.0.2", - "@typescript/typescript-netbsd-x64": "7.0.2", - "@typescript/typescript-openbsd-arm64": "7.0.2", - "@typescript/typescript-openbsd-x64": "7.0.2", - "@typescript/typescript-sunos-x64": "7.0.2", - "@typescript/typescript-win32-arm64": "7.0.2", - "@typescript/typescript-win32-x64": "7.0.2" - } - }, - "node_modules/undici-types": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", - "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/unified": { - "version": "11.0.5", - "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", - "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "bail": "^2.0.0", - "devlop": "^1.0.0", - "extend": "^3.0.0", - "is-plain-obj": "^4.0.0", - "trough": "^2.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-is": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", - "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", - "dev": true, - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-stringify-position": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", - "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", - "dev": true, - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-visit": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", - "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", - "dev": true, - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-is": "^6.0.0", - "unist-util-visit-parents": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-visit-parents": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", - "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", - "dev": true, - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-is": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vfile": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.1.tgz", - "integrity": "sha512-1bYqc7pt6NIADBJ98UiG0Bn/CHIVOoZ/IyEkqIruLg0mE1BKzkOXY2D6CSqQIcKqgadppE5lrxgWXJmXd7zZJw==", - "dev": true, - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-stringify-position": "^4.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vfile-message": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.2.tgz", - "integrity": "sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==", - "dev": true, - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-stringify-position": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vite": { - "version": "8.1.4", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.4.tgz", - "integrity": "sha512-bTT9PsdWO+MQMNG9ZXIP/qM9wGh37DFxTV/sPq9cFpHr3w4jkgef032PkAL9jAqhk3Nz8NQw3O8n6/xFkqO4QQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "lightningcss": "^1.32.0", - "picomatch": "^4.0.5", - "postcss": "^8.5.16", - "rolldown": "~1.1.4", - "tinyglobby": "^0.2.17" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.3.0", - "esbuild": "^0.27.0 || ^0.28.0", - "jiti": ">=1.21.0", - "less": "^4.0.0", - "sass": "^1.70.0", - "sass-embedded": "^1.70.0", - "stylus": ">=0.54.8", - "sugarss": "^5.0.0", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "@vitejs/devtools": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/vite-node": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-6.0.0.tgz", - "integrity": "sha512-oj4PVrT+pDh6GYf5wfUXkcZyekYS8kKPfLPXVl8qe324Ec6l4K2DUKNadRbZ3LQl0qGcDz+PyOo7ZAh00Y+JjQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "cac": "^7.0.0", - "es-module-lexer": "^2.0.0", - "obug": "^2.1.1", - "pathe": "^2.0.3", - "vite": "^8.0.0" - }, - "bin": { - "vite-node": "dist/cli.mjs" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "funding": { - "url": "https://opencollective.com/antfu" - } - }, - "node_modules/vitest": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz", - "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/expect": "4.1.10", - "@vitest/mocker": "4.1.10", - "@vitest/pretty-format": "4.1.10", - "@vitest/runner": "4.1.10", - "@vitest/snapshot": "4.1.10", - "@vitest/spy": "4.1.10", - "@vitest/utils": "4.1.10", - "es-module-lexer": "^2.0.0", - "expect-type": "^1.3.0", - "magic-string": "^0.30.21", - "obug": "^2.1.1", - "pathe": "^2.0.3", - "picomatch": "^4.0.3", - "std-env": "^4.0.0-rc.1", - "tinybench": "^2.9.0", - "tinyexec": "^1.0.2", - "tinyglobby": "^0.2.15", - "tinyrainbow": "^3.1.0", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", - "why-is-node-running": "^2.3.0" - }, - "bin": { - "vitest": "vitest.mjs" - }, - "engines": { - "node": "^20.0.0 || ^22.0.0 || >=24.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "@edge-runtime/vm": "*", - "@opentelemetry/api": "^1.9.0", - "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.10", - "@vitest/browser-preview": "4.1.10", - "@vitest/browser-webdriverio": "4.1.10", - "@vitest/coverage-istanbul": "4.1.10", - "@vitest/coverage-v8": "4.1.10", - "@vitest/ui": "4.1.10", - "happy-dom": "*", - "jsdom": "*", - "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" - }, - "peerDependenciesMeta": { - "@edge-runtime/vm": { - "optional": true - }, - "@opentelemetry/api": { - "optional": true - }, - "@types/node": { - "optional": true - }, - "@vitest/browser-playwright": { - "optional": true - }, - "@vitest/browser-preview": { - "optional": true - }, - "@vitest/browser-webdriverio": { - "optional": true - }, - "@vitest/coverage-istanbul": { - "optional": true - }, - "@vitest/coverage-v8": { - "optional": true - }, - "@vitest/ui": { - "optional": true - }, - "happy-dom": { - "optional": true - }, - "jsdom": { - "optional": true - }, - "vite": { - "optional": false - } - } - }, - "node_modules/why-is-node-running": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", - "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", - "dev": true, - "license": "MIT", - "dependencies": { - "siginfo": "^2.0.0", - "stackback": "0.0.2" - }, - "bin": { - "why-is-node-running": "cli.js" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, - "node_modules/zwitch": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", - "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", - "dev": true, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - } - } -} diff --git a/scripts/changelog-checker/package.json b/scripts/changelog-checker/package.json deleted file mode 100644 index f557a8c072..0000000000 --- a/scripts/changelog-checker/package.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "name": "changelog-checker", - "version": "1.0.0", - "description": "", - "type": "module", - "scripts": { - "run": "vite-node src/index.ts", - "test": "vitest run", - "test:coverage": "vitest run --coverage" - }, - "devDependencies": { - "@types/mdast": "4.0.4", - "@types/node": "26.1.1", - "@vitest/coverage-v8": "4.1.10", - "mdast-util-to-string": "4.0.0", - "remark": "15.0.1", - "remark-parse": "11.0.0", - "typescript": "7.0.2", - "unified": "11.0.5", - "vite": "8.1.4", - "vite-node": "6.0.0", - "vitest": "4.1.10" - } -} diff --git a/scripts/changelog-checker/vite.config.ts b/scripts/changelog-checker/vite.config.ts deleted file mode 100644 index 46db02c806..0000000000 --- a/scripts/changelog-checker/vite.config.ts +++ /dev/null @@ -1,6 +0,0 @@ -import {defineConfig} from 'vite'; - - -const config = defineConfig({}); - -export default config; From b2d07ca6e3bb3977c04b95e74016321de9f09137 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Tue, 21 Jul 2026 10:01:21 +0900 Subject: [PATCH 18/23] Update .coderabbit.yaml --- .coderabbit.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.coderabbit.yaml b/.coderabbit.yaml index 415c9ddcae..e7d070843c 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -15,3 +15,9 @@ reviews: - "dependabot[bot]" - "renovate[bot]" - "github-actions[bot]" + path_instructions: + - path: "**/*" + instructions: | + 【レビュー対象外】 + - フォーマット違反、型エラー、SPDXヘッダーの記載漏れ等、静的解析で検出可能な問題については、別Workflow(CI/CD)でチェックされるため、レビュー対象外として無視してください。 + - これらの問題についての指摘やコメントは行わないでください。 From ccd04fbcd9c686f7ac644a89e0e92143e6a1b70c Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Tue, 21 Jul 2026 10:03:23 +0900 Subject: [PATCH 19/23] Update .coderabbit.yaml --- .coderabbit.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.coderabbit.yaml b/.coderabbit.yaml index e7d070843c..007c507c7e 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -20,4 +20,5 @@ reviews: instructions: | 【レビュー対象外】 - フォーマット違反、型エラー、SPDXヘッダーの記載漏れ等、静的解析で検出可能な問題については、別Workflow(CI/CD)でチェックされるため、レビュー対象外として無視してください。 + - CHANGELOGに関する問題も無視してください。 - これらの問題についての指摘やコメントは行わないでください。 From 1dcd6c287cb55609f448764ea287c48e787e7876 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Tue, 21 Jul 2026 10:06:26 +0900 Subject: [PATCH 20/23] Update .coderabbit.yaml --- .coderabbit.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.coderabbit.yaml b/.coderabbit.yaml index 007c507c7e..033f614715 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -22,3 +22,6 @@ reviews: - フォーマット違反、型エラー、SPDXヘッダーの記載漏れ等、静的解析で検出可能な問題については、別Workflow(CI/CD)でチェックされるため、レビュー対象外として無視してください。 - CHANGELOGに関する問題も無視してください。 - これらの問題についての指摘やコメントは行わないでください。 + + 【注力してほしい観点】 + - ビジネスロジックの不備、セキュリティ、パフォーマンス、設計パターンの適切性などに焦点を当ててレビューしてください。 From f138a48419605af5b445cc353d0f234eed7866ef Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Tue, 21 Jul 2026 10:09:21 +0900 Subject: [PATCH 21/23] Update .coderabbit.yaml --- .coderabbit.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.coderabbit.yaml b/.coderabbit.yaml index 033f614715..b5a669a5c3 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -20,8 +20,8 @@ reviews: instructions: | 【レビュー対象外】 - フォーマット違反、型エラー、SPDXヘッダーの記載漏れ等、静的解析で検出可能な問題については、別Workflow(CI/CD)でチェックされるため、レビュー対象外として無視してください。 - - CHANGELOGに関する問題も無視してください。 - - これらの問題についての指摘やコメントは行わないでください。 【注力してほしい観点】 - ビジネスロジックの不備、セキュリティ、パフォーマンス、設計パターンの適切性などに焦点を当ててレビューしてください。 + path_filters: + - "!CHANGELOG.md" From 159b1a4484f56d5087ec29280a7ce5cc863a3e4d Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Tue, 21 Jul 2026 10:30:46 +0900 Subject: [PATCH 22/23] Update .coderabbit.yaml --- .coderabbit.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.coderabbit.yaml b/.coderabbit.yaml index b5a669a5c3..379e3f2607 100644 --- a/.coderabbit.yaml +++ b/.coderabbit.yaml @@ -25,3 +25,4 @@ reviews: - ビジネスロジックの不備、セキュリティ、パフォーマンス、設計パターンの適切性などに焦点を当ててレビューしてください。 path_filters: - "!CHANGELOG.md" + - "!**/__snapshots__/**" From b51d5ba0a085f1b72aa206ef64d1acc890185ec5 Mon Sep 17 00:00:00 2001 From: syuilo <4439005+syuilo@users.noreply.github.com> Date: Tue, 21 Jul 2026 11:26:53 +0900 Subject: [PATCH 23/23] =?UTF-8?q?frontend=20bundle=20diagnostics=20?= =?UTF-8?q?=E3=81=A8=20frontend=20browser=20diagnostics=20=E3=82=92?= =?UTF-8?q?=E7=B5=B1=E5=90=88=20(#17757)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor(gh): unify frontend diagnostics * refactor(gh): unify frontend diagnostics markdown rendering * docs(gh): restore frontend diagnostics comments * wip * fix * refactor: 呼称をbase/headに統一 * tweak * Update types.ts * fix --- .../frontend-browser-diagnostics.comment.yml | 44 ------ .../frontend-bundle-diagnostics.comment.yml | 44 ------ .../frontend-bundle-diagnostics.inspect.yml | 148 ------------------ .../frontend-diagnostics.comment.yml | 75 +++++++++ ...t.yml => frontend-diagnostics.inspect.yml} | 145 ++++++++++------- .github/workflows/packages-private.yml | 3 +- packages-private/README.md | 12 +- .../src/render-md.ts | 31 ---- .../test/render.test.ts | 69 -------- .../eslint.config.js | 25 --- .../diagnostics-frontend-bundle/package.json | 22 --- .../src/render-md.ts | 32 ---- .../diagnostics-frontend-bundle/src/report.ts | 33 ---- .../test/__snapshots__/render-md.md | 100 ------------ .../diagnostics-frontend-bundle/tsconfig.json | 20 --- .../eslint.config.js | 0 .../package.json | 6 +- .../src/browser/controller.ts | 4 +- .../src/browser/diagnostics.ts | 2 +- .../src/browser/network.ts | 2 +- .../src/browser}/report/html-styles.ts | 0 .../src/browser}/report/html.ts | 2 +- .../src/browser}/scenario.ts | 4 +- .../src/browser}/server.ts | 11 +- .../src/browser}/summarize.ts | 2 +- .../src/browser}/types.ts | 5 + .../src/bundle}/chunk-report.ts | 128 +++++++-------- .../src/bundle}/fs-utils.ts | 5 +- .../src/bundle}/manifest.ts | 27 ++-- .../src/bundle}/visualizer.ts | 22 +-- .../src/inspect-browser.ts} | 10 +- .../src/render-browser-html.ts} | 8 +- .../diagnostics-frontend/src/render-md.ts | 62 ++++++++ .../src/report.ts} | 54 +++++-- .../test/__snapshots__/report.md} | 101 +++++++++++- .../browser}/__snapshots__/render-html.html | 0 .../test/browser}/fixtures/base.json | 0 .../test/browser}/fixtures/head.json | 0 .../test/browser/render.test.ts | 44 ++++++ .../test/browser/server.test.ts | 47 ++++++ .../test/bundle/fixtures/base-stats.json} | 2 +- .../test/bundle/fixtures/head-stats.json} | 2 +- .../test/bundle/fs-utils.test.ts | 29 ++++ .../test/bundle/manifest.test.ts | 24 +++ .../test/report.test.ts} | 73 ++++++--- .../tsconfig.json | 0 pnpm-lock.yaml | 24 +-- 47 files changed, 693 insertions(+), 810 deletions(-) delete mode 100644 .github/workflows/frontend-browser-diagnostics.comment.yml delete mode 100644 .github/workflows/frontend-bundle-diagnostics.comment.yml delete mode 100644 .github/workflows/frontend-bundle-diagnostics.inspect.yml create mode 100644 .github/workflows/frontend-diagnostics.comment.yml rename .github/workflows/{frontend-browser-diagnostics.inspect.yml => frontend-diagnostics.inspect.yml} (51%) delete mode 100644 packages-private/diagnostics-frontend-browser/src/render-md.ts delete mode 100644 packages-private/diagnostics-frontend-browser/test/render.test.ts delete mode 100644 packages-private/diagnostics-frontend-bundle/eslint.config.js delete mode 100644 packages-private/diagnostics-frontend-bundle/package.json delete mode 100644 packages-private/diagnostics-frontend-bundle/src/render-md.ts delete mode 100644 packages-private/diagnostics-frontend-bundle/src/report.ts delete mode 100644 packages-private/diagnostics-frontend-bundle/test/__snapshots__/render-md.md delete mode 100644 packages-private/diagnostics-frontend-bundle/tsconfig.json rename packages-private/{diagnostics-frontend-browser => diagnostics-frontend}/eslint.config.js (100%) rename packages-private/{diagnostics-frontend-browser => diagnostics-frontend}/package.json (76%) rename packages-private/{diagnostics-frontend-browser => diagnostics-frontend}/src/browser/controller.ts (99%) rename packages-private/{diagnostics-frontend-browser => diagnostics-frontend}/src/browser/diagnostics.ts (92%) rename packages-private/{diagnostics-frontend-browser => diagnostics-frontend}/src/browser/network.ts (99%) rename packages-private/{diagnostics-frontend-browser/src => diagnostics-frontend/src/browser}/report/html-styles.ts (100%) rename packages-private/{diagnostics-frontend-browser/src => diagnostics-frontend/src/browser}/report/html.ts (98%) rename packages-private/{diagnostics-frontend-browser/src => diagnostics-frontend/src/browser}/scenario.ts (87%) rename packages-private/{diagnostics-frontend-browser/src => diagnostics-frontend/src/browser}/server.ts (90%) rename packages-private/{diagnostics-frontend-browser/src => diagnostics-frontend/src/browser}/summarize.ts (98%) rename packages-private/{diagnostics-frontend-browser/src => diagnostics-frontend/src/browser}/types.ts (87%) rename packages-private/{diagnostics-frontend-bundle/src => diagnostics-frontend/src/bundle}/chunk-report.ts (50%) rename packages-private/{diagnostics-frontend-bundle/src => diagnostics-frontend/src/bundle}/fs-utils.ts (90%) rename packages-private/{diagnostics-frontend-bundle/src => diagnostics-frontend/src/bundle}/manifest.ts (90%) rename packages-private/{diagnostics-frontend-bundle/src => diagnostics-frontend/src/bundle}/visualizer.ts (83%) rename packages-private/{diagnostics-frontend-browser/src/inspect.ts => diagnostics-frontend/src/inspect-browser.ts} (95%) rename packages-private/{diagnostics-frontend-browser/src/render-html.ts => diagnostics-frontend/src/render-browser-html.ts} (68%) create mode 100644 packages-private/diagnostics-frontend/src/render-md.ts rename packages-private/{diagnostics-frontend-browser/src/report/markdown.ts => diagnostics-frontend/src/report.ts} (86%) rename packages-private/{diagnostics-frontend-browser/test/__snapshots__/render-md.md => diagnostics-frontend/test/__snapshots__/report.md} (62%) rename packages-private/{diagnostics-frontend-browser/test => diagnostics-frontend/test/browser}/__snapshots__/render-html.html (100%) rename packages-private/{diagnostics-frontend-browser/test => diagnostics-frontend/test/browser}/fixtures/base.json (100%) rename packages-private/{diagnostics-frontend-browser/test => diagnostics-frontend/test/browser}/fixtures/head.json (100%) create mode 100644 packages-private/diagnostics-frontend/test/browser/render.test.ts create mode 100644 packages-private/diagnostics-frontend/test/browser/server.test.ts rename packages-private/{diagnostics-frontend-bundle/test/fixtures/before-stats.json => diagnostics-frontend/test/bundle/fixtures/base-stats.json} (99%) rename packages-private/{diagnostics-frontend-bundle/test/fixtures/after-stats.json => diagnostics-frontend/test/bundle/fixtures/head-stats.json} (98%) create mode 100644 packages-private/diagnostics-frontend/test/bundle/fs-utils.test.ts create mode 100644 packages-private/diagnostics-frontend/test/bundle/manifest.test.ts rename packages-private/{diagnostics-frontend-bundle/test/render-md.test.ts => diagnostics-frontend/test/report.test.ts} (53%) rename packages-private/{diagnostics-frontend-browser => diagnostics-frontend}/tsconfig.json (100%) diff --git a/.github/workflows/frontend-browser-diagnostics.comment.yml b/.github/workflows/frontend-browser-diagnostics.comment.yml deleted file mode 100644 index 5098e618ad..0000000000 --- a/.github/workflows/frontend-browser-diagnostics.comment.yml +++ /dev/null @@ -1,44 +0,0 @@ -name: Frontend browser diagnostics (comment) - -on: - workflow_run: - workflows: - - Frontend browser diagnostics (inspect) - types: - - completed - -permissions: - actions: read - contents: read - issues: write - pull-requests: write - -jobs: - comment: - name: Comment frontend browser diagnostics report - if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success' - runs-on: ubuntu-latest - concurrency: - group: frontend-browser-diagnostics-report-${{ github.event.workflow_run.id }} - cancel-in-progress: true - steps: - - name: Download browser metrics report - uses: actions/download-artifact@v8 - with: - name: frontend-browser-diagnostics - path: ${{ runner.temp }}/frontend-browser-diagnostics - github-token: ${{ github.token }} - repository: ${{ github.repository }} - run-id: ${{ github.event.workflow_run.id }} - - - name: Load PR number - id: load-pr-number - shell: bash - run: echo "pr-number=$(cat "$RUNNER_TEMP/frontend-browser-diagnostics/pr-number.txt")" >> "$GITHUB_OUTPUT" - - - name: Comment on pull request - uses: thollander/actions-comment-pull-request@v3 - with: - pr-number: ${{ steps.load-pr-number.outputs.pr-number }} - comment-tag: frontend-browser-diagnostics - file-path: ${{ runner.temp }}/frontend-browser-diagnostics/frontend-browser-diagnostics.md diff --git a/.github/workflows/frontend-bundle-diagnostics.comment.yml b/.github/workflows/frontend-bundle-diagnostics.comment.yml deleted file mode 100644 index 709c735e69..0000000000 --- a/.github/workflows/frontend-bundle-diagnostics.comment.yml +++ /dev/null @@ -1,44 +0,0 @@ -name: Frontend bundle diagnostics (comment) - -on: - workflow_run: - workflows: - - Frontend bundle diagnostics (inspect) - types: - - completed - -permissions: - actions: read - contents: read - issues: write - pull-requests: write - -jobs: - comment: - name: Comment frontend bundle report - if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success' - runs-on: ubuntu-latest - concurrency: - group: frontend-bundle-diagnostics-report-${{ github.event.workflow_run.id }} - cancel-in-progress: true - steps: - - name: Download bundle report - uses: actions/download-artifact@v8 - with: - name: frontend-bundle-report - path: ${{ runner.temp }}/frontend-bundle-report - github-token: ${{ github.token }} - repository: ${{ github.repository }} - run-id: ${{ github.event.workflow_run.id }} - - - name: Load PR number - id: load-pr-number - shell: bash - run: echo "pr-number=$(cat "$RUNNER_TEMP/frontend-bundle-report/pr-number.txt")" >> "$GITHUB_OUTPUT" - - - name: Comment on pull request - uses: thollander/actions-comment-pull-request@v3 - with: - pr-number: ${{ steps.load-pr-number.outputs.pr-number }} - comment-tag: frontend-bundle-diagnostics - file-path: ${{ runner.temp }}/frontend-bundle-report/frontend-bundle-diagnostics-report.md diff --git a/.github/workflows/frontend-bundle-diagnostics.inspect.yml b/.github/workflows/frontend-bundle-diagnostics.inspect.yml deleted file mode 100644 index 0332ea6a3e..0000000000 --- a/.github/workflows/frontend-bundle-diagnostics.inspect.yml +++ /dev/null @@ -1,148 +0,0 @@ -name: Frontend bundle diagnostics (inspect) - -on: - pull_request: - types: - - opened - - synchronize - - reopened - - ready_for_review - paths: - - packages/frontend/** - - packages/frontend-shared/** - - packages/frontend-builder/** - - packages/i18n/** - - packages/icons-subsetter/** - - packages/misskey-js/** - - packages/misskey-reversi/** - - packages/misskey-bubble-game/** - - package.json - - pnpm-lock.yaml - - pnpm-workspace.yaml - - .node-version - - packages-private/diagnostics-shared/** - - packages-private/diagnostics-frontend-bundle/** - - .github/workflows/frontend-bundle-diagnostics.inspect.yml - - .github/workflows/frontend-bundle-diagnostics.comment.yml - -permissions: - contents: read - pull-requests: read - -concurrency: - group: frontend-bundle-diagnostics-inspect-${{ github.event.pull_request.number }} - cancel-in-progress: true - -jobs: - report: - name: Build frontend bundle report - runs-on: ubuntu-latest - steps: - - name: Checkout base - uses: actions/checkout@v6.0.3 - with: - repository: ${{ github.event.pull_request.base.repo.full_name }} - ref: ${{ github.event.pull_request.base.sha }} - path: before - submodules: true - - - name: Checkout pull request - uses: actions/checkout@v6.0.3 - with: - repository: ${{ github.event.pull_request.head.repo.full_name }} - ref: ${{ github.event.pull_request.head.sha }} - path: after - submodules: true - - - name: Setup pnpm - uses: pnpm/action-setup@v6.0.9 - with: - package_json_file: after/package.json - - - name: Setup Node.js - uses: actions/setup-node@v6.4.0 - with: - node-version-file: after/.node-version - cache: pnpm - cache-dependency-path: | - before/pnpm-lock.yaml - after/pnpm-lock.yaml - - - name: Install dependencies for base - working-directory: before - run: pnpm i --frozen-lockfile - - - name: Build frontend dependencies for base - working-directory: before - run: pnpm --filter "frontend^..." run build - - - name: Prepare report output - run: mkdir -p "$RUNNER_TEMP/frontend-bundle-report" - - - name: Build frontend report for base - working-directory: before - env: - FRONTEND_BUNDLE_VISUALIZER: 'true' - FRONTEND_BUNDLE_VISUALIZER_FILE: ${{ runner.temp }}/frontend-bundle-report/before-stats.json - run: pnpm --filter frontend run build - - - name: Install dependencies for pull request - working-directory: after - run: pnpm i --frozen-lockfile - - - name: Build frontend dependencies for pull request - working-directory: after - run: pnpm --filter "frontend^..." run build - - - name: Build frontend report for pull request - working-directory: after - env: - FRONTEND_BUNDLE_VISUALIZER: 'true' - FRONTEND_BUNDLE_VISUALIZER_FILE: ${{ runner.temp }}/frontend-bundle-report/after-stats.json - FRONTEND_BUNDLE_VISUALIZER_HTML_FILE: ${{ runner.temp }}/frontend-bundle-report/frontend-bundle-visualizer.html - run: pnpm --filter frontend run build - - - name: Upload bundle visualizer - id: upload-bundle-visualizer - uses: actions/upload-artifact@v7 - with: - name: frontend-bundle-visualizer - path: ${{ runner.temp }}/frontend-bundle-report/frontend-bundle-visualizer.html - if-no-files-found: error - archive: false - retention-days: 7 - - - name: Generate report markdown - shell: bash - working-directory: after - env: - BASE_SHA: ${{ github.event.pull_request.base.sha }} - HEAD_SHA: ${{ github.event.pull_request.head.sha }} - PR_NUMBER: ${{ github.event.pull_request.number }} - FRONTEND_BUNDLE_REPORT_ARTIFACT_URL: ${{ steps.upload-bundle-visualizer.outputs.artifact-url }} - run: | - REPORT_DIR="$RUNNER_TEMP/frontend-bundle-report" - pnpm --filter diagnostics-frontend-bundle run render-md \ - "$GITHUB_WORKSPACE/before" "$GITHUB_WORKSPACE/after" \ - "$REPORT_DIR/before-stats.json" "$REPORT_DIR/after-stats.json" \ - "$REPORT_DIR/frontend-bundle-diagnostics-report.md" - printf '%s\n' "$PR_NUMBER" > "$REPORT_DIR/pr-number.txt" - printf '%s\n' "$BASE_SHA" > "$REPORT_DIR/base-sha.txt" - printf '%s\n' "$HEAD_SHA" > "$REPORT_DIR/head-sha.txt" - printf '%s\n' "${{ github.event.pull_request.html_url }}" > "$REPORT_DIR/pr-url.txt" - - - name: Check report - run: | - REPORT_DIR="$RUNNER_TEMP/frontend-bundle-report" - test -s "$REPORT_DIR/before-stats.json" - test -s "$REPORT_DIR/after-stats.json" - test -s "$REPORT_DIR/frontend-bundle-diagnostics-report.md" - cat "$REPORT_DIR/frontend-bundle-diagnostics-report.md" >> "$GITHUB_STEP_SUMMARY" - - - name: Upload bundle report - uses: actions/upload-artifact@v7 - with: - name: frontend-bundle-report - path: ${{ runner.temp }}/frontend-bundle-report/ - if-no-files-found: error - retention-days: 7 diff --git a/.github/workflows/frontend-diagnostics.comment.yml b/.github/workflows/frontend-diagnostics.comment.yml new file mode 100644 index 0000000000..96deddcc58 --- /dev/null +++ b/.github/workflows/frontend-diagnostics.comment.yml @@ -0,0 +1,75 @@ +name: Frontend diagnostics (comment) + +on: + workflow_run: + workflows: + - Frontend diagnostics (inspect) + types: + - completed + +permissions: + actions: read + contents: read + issues: write + pull-requests: write + +jobs: + comment: + name: Comment frontend diagnostics report + if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success' + runs-on: ubuntu-latest + concurrency: + group: frontend-diagnostics-report-${{ github.event.workflow_run.id }} + cancel-in-progress: true + steps: + - name: Download frontend diagnostics report + uses: actions/download-artifact@v8 + with: + name: frontend-diagnostics + path: ${{ runner.temp }}/frontend-diagnostics + github-token: ${{ github.token }} + repository: ${{ github.repository }} + run-id: ${{ github.event.workflow_run.id }} + + - name: Resolve current pull request + id: resolve-pr + uses: actions/github-script@v9 + env: + PR_NUMBER_FILE: ${{ runner.temp }}/frontend-diagnostics/pr-number.txt + with: + script: | + const fs = require('fs/promises'); + const { owner, repo } = context.repo; + const workflowRun = context.payload.workflow_run; + const pullRequestNumberText = (await fs.readFile(process.env.PR_NUMBER_FILE, 'utf8')).trim(); + if (!/^[1-9]\d*$/.test(pullRequestNumberText)) { + throw new Error('Invalid pull request number in frontend diagnostics artifact.'); + } + + const pullRequestNumber = Number(pullRequestNumberText); + if (!Number.isSafeInteger(pullRequestNumber)) { + throw new Error('Pull request number in frontend diagnostics artifact is not a safe integer.'); + } + + const { data: currentPullRequest } = await github.rest.pulls.get({ + owner, + repo, + pull_number: pullRequestNumber, + }); + + if (currentPullRequest.state !== 'open' || currentPullRequest.head.sha !== workflowRun.head_sha) { + core.notice(`Skipping stale frontend diagnostics report for pull request #${pullRequestNumber}.`); + core.setOutput('is-current', 'false'); + return; + } + + core.setOutput('is-current', 'true'); + core.setOutput('pr-number', String(pullRequestNumber)); + + - name: Comment on pull request + if: steps.resolve-pr.outputs.is-current == 'true' + uses: thollander/actions-comment-pull-request@v3 + with: + pr-number: ${{ steps.resolve-pr.outputs.pr-number }} + comment-tag: frontend-diagnostics + file-path: ${{ runner.temp }}/frontend-diagnostics/frontend-diagnostics.md diff --git a/.github/workflows/frontend-browser-diagnostics.inspect.yml b/.github/workflows/frontend-diagnostics.inspect.yml similarity index 51% rename from .github/workflows/frontend-browser-diagnostics.inspect.yml rename to .github/workflows/frontend-diagnostics.inspect.yml index 7b6238c81d..ecf06c3092 100644 --- a/.github/workflows/frontend-browser-diagnostics.inspect.yml +++ b/.github/workflows/frontend-diagnostics.inspect.yml @@ -1,4 +1,4 @@ -name: Frontend browser diagnostics (inspect) +name: Frontend diagnostics (inspect) on: pull_request: @@ -23,21 +23,21 @@ on: - .node-version - .github/misskey/test.yml - packages-private/diagnostics-shared/** - - packages-private/diagnostics-frontend-browser/** - - .github/workflows/frontend-browser-diagnostics.inspect.yml - - .github/workflows/frontend-browser-diagnostics.comment.yml + - packages-private/diagnostics-frontend/** + - .github/workflows/frontend-diagnostics.inspect.yml + - .github/workflows/frontend-diagnostics.comment.yml permissions: contents: read pull-requests: read concurrency: - group: frontend-browser-diagnostics-inspect-${{ github.event.pull_request.number }} + group: frontend-diagnostics-inspect-${{ github.event.pull_request.number }} cancel-in-progress: true jobs: report: - name: Measure frontend browser metrics + name: Measure frontend diagnostics runs-on: ubuntu-latest timeout-minutes: 90 @@ -61,79 +61,100 @@ jobs: persist-credentials: false repository: ${{ github.event.pull_request.base.repo.full_name }} ref: ${{ github.event.pull_request.base.sha }} - path: before + path: base submodules: true - - name: Checkout pull request + - name: Checkout head uses: actions/checkout@v6.0.3 with: persist-credentials: false repository: ${{ github.event.pull_request.head.repo.full_name }} ref: ${{ github.event.pull_request.head.sha }} - path: after + path: head submodules: true - name: Setup pnpm uses: pnpm/action-setup@v6.0.9 with: - package_json_file: after/package.json + package_json_file: head/package.json - name: Setup Node.js uses: actions/setup-node@v6.4.0 with: - node-version-file: after/.node-version + node-version-file: head/.node-version cache: pnpm cache-dependency-path: | - before/pnpm-lock.yaml - after/pnpm-lock.yaml + base/pnpm-lock.yaml + head/pnpm-lock.yaml + + - name: Prepare report output + shell: bash + run: mkdir -p "$RUNNER_TEMP/frontend-diagnostics" - name: Install dependencies for base - working-directory: before + working-directory: base run: pnpm i --frozen-lockfile - name: Configure base - working-directory: before + working-directory: base run: cp .github/misskey/test.yml .config - name: Build base - working-directory: before + working-directory: base + env: + FRONTEND_BUNDLE_VISUALIZER: 'true' + FRONTEND_BUNDLE_VISUALIZER_FILE: ${{ runner.temp }}/frontend-diagnostics/base-bundle-stats.json run: pnpm build - - name: Install dependencies for pull request - working-directory: after + - name: Install dependencies for head + working-directory: head run: pnpm i --frozen-lockfile - - name: Configure pull request - working-directory: after + - name: Configure head + working-directory: head run: cp .github/misskey/test.yml .config - - name: Build pull request - working-directory: after + - name: Build head + working-directory: head + env: + FRONTEND_BUNDLE_VISUALIZER: 'true' + FRONTEND_BUNDLE_VISUALIZER_FILE: ${{ runner.temp }}/frontend-diagnostics/head-bundle-stats.json + FRONTEND_BUNDLE_VISUALIZER_HTML_FILE: ${{ runner.temp }}/frontend-diagnostics/frontend-bundle-visualizer.html run: pnpm build + - name: Upload bundle visualizer + id: upload-bundle-visualizer + uses: actions/upload-artifact@v7 + with: + name: frontend-bundle-visualizer + path: ${{ runner.temp }}/frontend-diagnostics/frontend-bundle-visualizer.html + if-no-files-found: error + archive: false + retention-days: 7 + - name: Install Playwright browsers - working-directory: after/packages-private/diagnostics-frontend-browser + working-directory: head/packages-private/diagnostics-frontend run: pnpm exec playwright install --with-deps --no-shell chromium - name: Measure frontend browser metrics shell: bash - working-directory: after + working-directory: head env: FRONTEND_BROWSER_METRICS_SAMPLE_COUNT: 5 MK_ENABLE_CROSS_ORIGIN_ISOLATION: "true" - # 計測ハーネスはafter側のものを両方に使うが、成果物はrunnerのworkspace直下に出す + # 計測ハーネスはhead側のものを両方に使うが、成果物はrunnerのworkspace直下に出す FRONTEND_BROWSER_HEAP_SNAPSHOT_OUTPUT_DIR: ${{ github.workspace }} run: | - REPORT_DIR="$RUNNER_TEMP/frontend-browser-diagnostics" - mkdir -p "$REPORT_DIR" - pnpm --filter diagnostics-frontend-browser run inspect \ - "$GITHUB_WORKSPACE/before" "$GITHUB_WORKSPACE/after" \ - "$REPORT_DIR/before-browser.json" "$REPORT_DIR/after-browser.json" + REPORT_DIR="$RUNNER_TEMP/frontend-diagnostics" + pnpm --filter diagnostics-frontend run inspect-browser \ + "$GITHUB_WORKSPACE/base" "$GITHUB_WORKSPACE/head" \ + "$REPORT_DIR/base-browser.json" "$REPORT_DIR/head-browser.json" - name: Upload browser base heap snapshot id: upload-browser-base-heap-snapshot uses: actions/upload-artifact@v7 with: + name: frontend-browser-base-heap-snapshot path: base-heap-snapshot.heapsnapshot archive: false if-no-files-found: error @@ -143,6 +164,7 @@ jobs: id: upload-browser-head-heap-snapshot uses: actions/upload-artifact@v7 with: + name: frontend-browser-head-heap-snapshot path: head-heap-snapshot.heapsnapshot archive: false if-no-files-found: error @@ -150,13 +172,13 @@ jobs: - name: Generate browser detailed html shell: bash - working-directory: after + working-directory: head run: | - REPORT_DIR="$RUNNER_TEMP/frontend-browser-diagnostics" - test -s "$REPORT_DIR/before-browser.json" - test -s "$REPORT_DIR/after-browser.json" - pnpm --filter diagnostics-frontend-browser run render-html \ - "$REPORT_DIR/before-browser.json" "$REPORT_DIR/after-browser.json" \ + REPORT_DIR="$RUNNER_TEMP/frontend-diagnostics" + test -s "$REPORT_DIR/base-browser.json" + test -s "$REPORT_DIR/head-browser.json" + pnpm --filter diagnostics-frontend run render-browser-html \ + "$REPORT_DIR/base-browser.json" "$REPORT_DIR/head-browser.json" \ "$REPORT_DIR/frontend-browser-detailed-html.html" - name: Upload browser detailed html @@ -164,54 +186,61 @@ jobs: uses: actions/upload-artifact@v7 with: name: frontend-browser-metrics-detailed-html - path: ${{ runner.temp }}/frontend-browser-diagnostics/frontend-browser-detailed-html.html + path: ${{ runner.temp }}/frontend-diagnostics/frontend-browser-detailed-html.html if-no-files-found: error archive: false retention-days: 7 - - name: Generate browser metrics report + - name: Generate frontend diagnostics report shell: bash - working-directory: after + working-directory: head env: BASE_SHA: ${{ github.event.pull_request.base.sha }} HEAD_SHA: ${{ github.event.pull_request.head.sha }} PR_NUMBER: ${{ github.event.pull_request.number }} + FRONTEND_BUNDLE_REPORT_ARTIFACT_URL: ${{ steps.upload-bundle-visualizer.outputs.artifact-url }} FRONTEND_BROWSER_BASE_HEAP_SNAPSHOT_ARTIFACT_URL: ${{ steps.upload-browser-base-heap-snapshot.outputs.artifact-url }} FRONTEND_BROWSER_HEAD_HEAP_SNAPSHOT_ARTIFACT_URL: ${{ steps.upload-browser-head-heap-snapshot.outputs.artifact-url }} FRONTEND_BROWSER_DETAILED_HTML_ARTIFACT_URL: ${{ steps.upload-browser-detailed-html.outputs.artifact-url }} run: | - REPORT_DIR="$RUNNER_TEMP/frontend-browser-diagnostics" - test -s "$REPORT_DIR/before-browser.json" - test -s "$REPORT_DIR/after-browser.json" - pnpm --filter diagnostics-frontend-browser run render-md \ - "$REPORT_DIR/before-browser.json" "$REPORT_DIR/after-browser.json" \ - "$REPORT_DIR/frontend-browser-diagnostics.md" + REPORT_DIR="$RUNNER_TEMP/frontend-diagnostics" + test -s "$REPORT_DIR/base-bundle-stats.json" + test -s "$REPORT_DIR/head-bundle-stats.json" + test -s "$REPORT_DIR/base-browser.json" + test -s "$REPORT_DIR/head-browser.json" + pnpm --filter diagnostics-frontend run render-md \ + "$GITHUB_WORKSPACE/base" "$GITHUB_WORKSPACE/head" \ + "$REPORT_DIR/base-bundle-stats.json" "$REPORT_DIR/head-bundle-stats.json" \ + "$REPORT_DIR/base-browser.json" "$REPORT_DIR/head-browser.json" \ + "$REPORT_DIR/frontend-diagnostics.md" printf '%s\n' "$PR_NUMBER" > "$REPORT_DIR/pr-number.txt" printf '%s\n' "$BASE_SHA" > "$REPORT_DIR/base-sha.txt" printf '%s\n' "$HEAD_SHA" > "$REPORT_DIR/head-sha.txt" printf '%s\n' "${{ github.event.pull_request.html_url }}" > "$REPORT_DIR/pr-url.txt" - - name: Check browser metrics report + - name: Check frontend diagnostics report shell: bash run: | - REPORT_DIR="$RUNNER_TEMP/frontend-browser-diagnostics" - test -s "$REPORT_DIR/frontend-browser-diagnostics.md" + REPORT_DIR="$RUNNER_TEMP/frontend-diagnostics" + test -s "$REPORT_DIR/frontend-diagnostics.md" test -s "$REPORT_DIR/frontend-browser-detailed-html.html" test -s "$REPORT_DIR/pr-number.txt" test -s "$REPORT_DIR/head-sha.txt" - cat "$REPORT_DIR/frontend-browser-diagnostics.md" >> "$GITHUB_STEP_SUMMARY" + cat "$REPORT_DIR/frontend-diagnostics.md" >> "$GITHUB_STEP_SUMMARY" - - name: Upload browser metrics report + - name: Upload frontend diagnostics report uses: actions/upload-artifact@v7 with: - name: frontend-browser-diagnostics + name: frontend-diagnostics path: | - ${{ runner.temp }}/frontend-browser-diagnostics/before-browser.json - ${{ runner.temp }}/frontend-browser-diagnostics/after-browser.json - ${{ runner.temp }}/frontend-browser-diagnostics/frontend-browser-diagnostics.md - ${{ runner.temp }}/frontend-browser-diagnostics/pr-number.txt - ${{ runner.temp }}/frontend-browser-diagnostics/base-sha.txt - ${{ runner.temp }}/frontend-browser-diagnostics/head-sha.txt - ${{ runner.temp }}/frontend-browser-diagnostics/pr-url.txt + ${{ runner.temp }}/frontend-diagnostics/base-bundle-stats.json + ${{ runner.temp }}/frontend-diagnostics/head-bundle-stats.json + ${{ runner.temp }}/frontend-diagnostics/base-browser.json + ${{ runner.temp }}/frontend-diagnostics/head-browser.json + ${{ runner.temp }}/frontend-diagnostics/frontend-diagnostics.md + ${{ runner.temp }}/frontend-diagnostics/pr-number.txt + ${{ runner.temp }}/frontend-diagnostics/base-sha.txt + ${{ runner.temp }}/frontend-diagnostics/head-sha.txt + ${{ runner.temp }}/frontend-diagnostics/pr-url.txt if-no-files-found: error retention-days: 7 diff --git a/.github/workflows/packages-private.yml b/.github/workflows/packages-private.yml index e6a72b68c9..cf7e50670a 100644 --- a/.github/workflows/packages-private.yml +++ b/.github/workflows/packages-private.yml @@ -33,8 +33,7 @@ jobs: workspace: - diagnostics-shared - diagnostics-backend - - diagnostics-frontend-browser - - diagnostics-frontend-bundle + - diagnostics-frontend - changelog-checker steps: - uses: actions/checkout@v6.0.3 diff --git a/packages-private/README.md b/packages-private/README.md index faba8fa046..0c14dffdf5 100644 --- a/packages-private/README.md +++ b/packages-private/README.md @@ -8,8 +8,7 @@ | --- | --- | | [`diagnostics-shared`](./diagnostics-shared) | 計測系パッケージが共有する統計・書式整形・V8 heap snapshot解析のユーティリティ | | [`diagnostics-backend`](./diagnostics-backend) | バックエンドのメモリ使用量をbase/headで比較し、PRコメント用のMarkdownを生成する | -| [`diagnostics-frontend-browser`](./diagnostics-frontend-browser) | ヘッドレスChromeでフロントエンドを操作し、メモリ・ネットワーク等の指標をbase/headで比較する | -| [`diagnostics-frontend-bundle`](./diagnostics-frontend-bundle) | フロントエンドのビルド成果物のchunkサイズをbase/headで比較する | +| [`diagnostics-frontend`](./diagnostics-frontend) | フロントエンドをブラウザで起動した際のメトリクスと生成されたchunk情報等をbase/headで比較する | | [`changelog-checker`](./changelog-checker) | `CHANGELOG.md` の追記内容を検証する | ## GitHub Actionsからの呼び出し方 @@ -20,11 +19,12 @@ CLIに渡すパスはすべて呼び出し側のカレントディレクトリ ```yaml - name: Generate report - working-directory: after + working-directory: head run: >- - pnpm --filter diagnostics-frontend-bundle run render-md - "$GITHUB_WORKSPACE/before" "$GITHUB_WORKSPACE/after" - "$REPORT_DIR/before-stats.json" "$REPORT_DIR/after-stats.json" + pnpm --filter diagnostics-frontend run render-md + "$GITHUB_WORKSPACE/base" "$GITHUB_WORKSPACE/head" + "$REPORT_DIR/base-bundle-stats.json" "$REPORT_DIR/head-bundle-stats.json" + "$REPORT_DIR/base-browser.json" "$REPORT_DIR/head-browser.json" "$REPORT_DIR/report.md" ``` diff --git a/packages-private/diagnostics-frontend-browser/src/render-md.ts b/packages-private/diagnostics-frontend-browser/src/render-md.ts deleted file mode 100644 index 15c4924808..0000000000 --- a/packages-private/diagnostics-frontend-browser/src/render-md.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { readFile, writeFile } from 'node:fs/promises'; -import { resolve } from 'node:path'; -import { readOptionalEnv, readRequiredEnv } from 'diagnostics-shared/env'; -import { renderMarkdown } from './report/markdown'; -import type { BrowserMetricsReport } from './types'; - -async function main() { - const [baseFileArg, headFileArg, outputFileArg] = process.argv.slice(2); - if (baseFileArg == null || headFileArg == null || outputFileArg == null) { - throw new Error('Usage: render-md '); - } - - const base = JSON.parse(await readFile(resolve(baseFileArg), 'utf8')) as BrowserMetricsReport; - const head = JSON.parse(await readFile(resolve(headFileArg), 'utf8')) as BrowserMetricsReport; - - await writeFile(resolve(outputFileArg), renderMarkdown(base, head, { - baseHeapSnapshotUrl: readRequiredEnv('FRONTEND_BROWSER_BASE_HEAP_SNAPSHOT_ARTIFACT_URL'), - headHeapSnapshotUrl: readRequiredEnv('FRONTEND_BROWSER_HEAD_HEAP_SNAPSHOT_ARTIFACT_URL'), - detailedHtmlUrl: readOptionalEnv('FRONTEND_BROWSER_DETAILED_HTML_ARTIFACT_URL'), - })); -} - -await main().catch(err => { - console.error(err); - process.exit(1); -}); diff --git a/packages-private/diagnostics-frontend-browser/test/render.test.ts b/packages-private/diagnostics-frontend-browser/test/render.test.ts deleted file mode 100644 index 29e38f7b3e..0000000000 --- a/packages-private/diagnostics-frontend-browser/test/render.test.ts +++ /dev/null @@ -1,69 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { readFile } from 'node:fs/promises'; -import { join } from 'node:path'; -import { afterEach, beforeEach, expect, test, vi } from 'vitest'; -import { renderHtml } from '../src/report/html'; -import { renderMarkdown } from '../src/report/markdown'; -import type { BrowserMetricsReport } from '../src/types'; - -const fixturesDir = join(import.meta.dirname, 'fixtures'); - -async function loadFixture(name: string) { - return JSON.parse(await readFile(join(fixturesDir, `${name}.json`), 'utf8')) as BrowserMetricsReport; -} - -beforeEach(() => { - // renderHtml() が生成時刻を埋め込むので、スナップショットが揺れないよう時刻を固定する - vi.useFakeTimers(); - vi.setSystemTime(new Date('2026-07-18T00:00:00.000Z')); -}); - -afterEach(() => { - vi.useRealTimers(); -}); - -/** - * 出力をゴールデンファイルで固定する。 - * 意図的に変更したときは `vitest -u` で更新し、__snapshots__ の差分もレビューすること。 - */ -test('renders the browser diagnostics markdown report', async () => { - const markdown = renderMarkdown(await loadFixture('base'), await loadFixture('head'), { - baseHeapSnapshotUrl: 'https://example.invalid/base', - headHeapSnapshotUrl: 'https://example.invalid/head', - detailedHtmlUrl: 'https://example.invalid/html', - }); - - await expect(markdown).toMatchFileSnapshot('./__snapshots__/render-md.md'); -}); - -test('omits the details link when no detailed html artifact was uploaded', async () => { - const markdown = renderMarkdown(await loadFixture('base'), await loadFixture('head'), { - baseHeapSnapshotUrl: 'https://example.invalid/base', - headHeapSnapshotUrl: 'https://example.invalid/head', - detailedHtmlUrl: null, - }); - - expect(markdown).not.toContain('View details'); -}); - -test('renders the network request diff html report', async () => { - const html = renderHtml(await loadFixture('base'), await loadFixture('head')); - - await expect(html).toMatchFileSnapshot('./__snapshots__/render-html.html'); -}); - -test('escapes html metacharacters coming from the browser session', async () => { - const base = await loadFixture('base'); - const head = await loadFixture('head'); - // CDP由来の値は原理的には任意の文字列になりうるので、生HTMLとして出ないことを確かめる - head.samples[0].networkRequests[0].url = 'http://127.0.0.1:61812/">'; - - const html = renderHtml(base, head); - - expect(html).not.toContain(''); - expect(html).toContain('<script>alert(1)</script>'); -}); diff --git a/packages-private/diagnostics-frontend-bundle/eslint.config.js b/packages-private/diagnostics-frontend-bundle/eslint.config.js deleted file mode 100644 index 11bc648243..0000000000 --- a/packages-private/diagnostics-frontend-bundle/eslint.config.js +++ /dev/null @@ -1,25 +0,0 @@ -import tsParser from '@typescript-eslint/parser'; -import sharedConfig from '../../packages/shared/eslint.config.js'; - -// eslint-disable-next-line import/no-default-export -export default [ - ...sharedConfig, - { - ignores: [ - '**/node_modules', - '**/__snapshots__', - 'test/fixtures', - ], - }, - { - files: ['**/*.ts', '**/*.tsx'], - languageOptions: { - parserOptions: { - parser: tsParser, - project: ['./tsconfig.json'], - sourceType: 'module', - tsconfigRootDir: import.meta.dirname, - }, - }, - }, -]; diff --git a/packages-private/diagnostics-frontend-bundle/package.json b/packages-private/diagnostics-frontend-bundle/package.json deleted file mode 100644 index b0271a8101..0000000000 --- a/packages-private/diagnostics-frontend-bundle/package.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "diagnostics-frontend-bundle", - "private": true, - "type": "module", - "scripts": { - "eslint": "eslint './**/*.{js,jsx,ts,tsx}'", - "lint": "pnpm typecheck && pnpm eslint", - "render-md": "tsx src/render-md.ts", - "test": "vitest run", - "typecheck": "tsc --noEmit" - }, - "dependencies": { - "diagnostics-shared": "workspace:*" - }, - "devDependencies": { - "@types/node": "26.1.1", - "tsx": "4.23.1", - "typescript": "5.9.3", - "vite": "8.1.4", - "vitest": "4.1.10" - } -} diff --git a/packages-private/diagnostics-frontend-bundle/src/render-md.ts b/packages-private/diagnostics-frontend-bundle/src/render-md.ts deleted file mode 100644 index d8fe0997dd..0000000000 --- a/packages-private/diagnostics-frontend-bundle/src/render-md.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { promises as fs } from 'node:fs'; -import path from 'node:path'; -import { readRequiredEnv } from 'diagnostics-shared/env'; -import { collectReport } from './manifest'; -import { renderBundleReportMarkdown } from './report'; -import type { VisualizerReport } from './visualizer'; - -async function main() { - const [beforeDir, afterDir, beforeStatsFile, afterStatsFile, outFile] = process.argv.slice(2).map(arg => path.resolve(arg)); - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (outFile == null) throw new Error('Usage: render-md '); - - // 未設定のまま `undefined` という文字列をコメントに埋め込まないよう、ここで落とす - const visualizerArtifactUrl = readRequiredEnv('FRONTEND_BUNDLE_REPORT_ARTIFACT_URL'); - - const before = await collectReport(beforeDir); - const after = await collectReport(afterDir); - const beforeStats = JSON.parse(await fs.readFile(beforeStatsFile, 'utf8')) as VisualizerReport; - const afterStats = JSON.parse(await fs.readFile(afterStatsFile, 'utf8')) as VisualizerReport; - - await fs.writeFile(outFile, renderBundleReportMarkdown(before, after, beforeStats, afterStats, { visualizerArtifactUrl })); -} - -await main().catch(err => { - console.error(err); - process.exit(1); -}); diff --git a/packages-private/diagnostics-frontend-bundle/src/report.ts b/packages-private/diagnostics-frontend-bundle/src/report.ts deleted file mode 100644 index db2e1cf6e3..0000000000 --- a/packages-private/diagnostics-frontend-bundle/src/report.ts +++ /dev/null @@ -1,33 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { renderFrontendChunkReport } from './chunk-report'; -import { collectVisualizerReport, renderVisualizerSummaryTable, type VisualizerReport } from './visualizer'; -import type { CollectedReport } from './manifest'; - -export type RenderBundleReportOptions = { - /** rollup-plugin-visualizer が出力したtreemap HTMLのartifact URL */ - visualizerArtifactUrl: string; -}; - -export function renderBundleReportMarkdown( - before: CollectedReport, - after: CollectedReport, - beforeStats: VisualizerReport, - afterStats: VisualizerReport, - options: RenderBundleReportOptions, -) { - return [ - '## 📦 Frontend Bundle Report', - '', - renderFrontendChunkReport(before, after), - '', - '## Bundle Stats', - '', - renderVisualizerSummaryTable(collectVisualizerReport(beforeStats), collectVisualizerReport(afterStats)), - '', - `[Open treemap HTML](${options.visualizerArtifactUrl})`, - ].join('\n'); -} diff --git a/packages-private/diagnostics-frontend-bundle/test/__snapshots__/render-md.md b/packages-private/diagnostics-frontend-bundle/test/__snapshots__/render-md.md deleted file mode 100644 index 867cbf3b75..0000000000 --- a/packages-private/diagnostics-frontend-bundle/test/__snapshots__/render-md.md +++ /dev/null @@ -1,100 +0,0 @@ -## 📦 Frontend Bundle Report - -
    -Chunk size diff (2 updated, 0 added, 0 removed) - -| Chunk | Before | After | Δ | Δ (%) | -| --- | ---: | ---: | ---: | ---: | -| (total) | 120 KB | 127 KB | $\color{orange}{\text{+6.3 KB}}$ | $\color{orange}{\text{+5.2\\%}}$ | -| | | | | | -|
    `vue` `assets/vue-b2.js`
    | 90 KB | 96 KB | $\color{orange}{\text{+6 KB}}$ | $\color{orange}{\text{+6.7\\%}}$ | -| (other generated chunks) | 1.2 KB | 1.5 KB | $\text{+300 B}$ | $\color{orange}{\text{+25\\%}}$ | -| (other) | 20 KB | 20 KB | $\text{+3 B}$ | $\text{+0\\%}$ | - -
    - -
    -Startup chunk size (2 updated, 0 added, 0 removed) - -| Chunk | Before | After | Δ | Δ (%) | -| --- | ---: | ---: | ---: | ---: | -| (total) | 114 KB | 120 KB | $\color{orange}{\text{+6 KB}}$ | $\color{orange}{\text{+5.3\\%}}$ | -| | | | | | -|
    `vue` `assets/vue-b2.js`
    | 90 KB | 96 KB | $\color{orange}{\text{+6 KB}}$ | $\color{orange}{\text{+6.7\\%}}$ | -| (other) | 24 KB | 24 KB | $\text{+3 B}$ | $\text{+0\\%}$ | - -_Startup chunks are the Vite entry for `src/_boot_.ts` and its static imports._ - -
    - - -## Bundle Stats - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    BundlesModulesEntriesImportsSize
    StaticDynamicRenderedGzipBrotli
    Before2612321 KB6.3 KB5.3 KB
    After2713331 KB8.4 KB7 KB
    Δ0$\color{orange}{\text{+1}}$0$\color{orange}{\text{+1}}$0$\color{orange}{\text{+9.8 KB}}$$\color{orange}{\text{+2.1 KB}}$$\color{orange}{\text{+1.8 KB}}$
    Δ (%)0%$\color{orange}{\text{+16.7\%}}$0%$\color{orange}{\text{+50\%}}$0%$\color{orange}{\text{+46.7\%}}$$\color{orange}{\text{+33.3\%}}$$\color{orange}{\text{+33.3\%}}$
    - -[Open treemap HTML](https://example.invalid/treemap) \ No newline at end of file diff --git a/packages-private/diagnostics-frontend-bundle/tsconfig.json b/packages-private/diagnostics-frontend-bundle/tsconfig.json deleted file mode 100644 index 6672c40acb..0000000000 --- a/packages-private/diagnostics-frontend-bundle/tsconfig.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "$schema": "https://json.schemastore.org/tsconfig", - "compilerOptions": { - "target": "ESNext", - "module": "ESNext", - "moduleResolution": "bundler", - "strict": true, - "strictFunctionTypes": true, - "strictNullChecks": true, - "esModuleInterop": true, - "skipLibCheck": true, - "noEmit": true, - "types": ["node"] - }, - "include": [ - "src/**/*.ts", - "test/**/*.ts" - ], - "exclude": [] -} diff --git a/packages-private/diagnostics-frontend-browser/eslint.config.js b/packages-private/diagnostics-frontend/eslint.config.js similarity index 100% rename from packages-private/diagnostics-frontend-browser/eslint.config.js rename to packages-private/diagnostics-frontend/eslint.config.js diff --git a/packages-private/diagnostics-frontend-browser/package.json b/packages-private/diagnostics-frontend/package.json similarity index 76% rename from packages-private/diagnostics-frontend-browser/package.json rename to packages-private/diagnostics-frontend/package.json index 9264dc49f9..8d3d2d248c 100644 --- a/packages-private/diagnostics-frontend-browser/package.json +++ b/packages-private/diagnostics-frontend/package.json @@ -1,12 +1,12 @@ { - "name": "diagnostics-frontend-browser", + "name": "diagnostics-frontend", "private": true, "type": "module", "scripts": { "eslint": "eslint './**/*.{js,jsx,ts,tsx}'", - "inspect": "tsx src/inspect.ts", + "inspect-browser": "tsx src/inspect-browser.ts", "lint": "pnpm typecheck && pnpm eslint", - "render-html": "tsx src/render-html.ts", + "render-browser-html": "tsx src/render-browser-html.ts", "render-md": "tsx src/render-md.ts", "test": "vitest run", "typecheck": "tsc --noEmit" diff --git a/packages-private/diagnostics-frontend-browser/src/browser/controller.ts b/packages-private/diagnostics-frontend/src/browser/controller.ts similarity index 99% rename from packages-private/diagnostics-frontend-browser/src/browser/controller.ts rename to packages-private/diagnostics-frontend/src/browser/controller.ts index 756245c636..dd17be2a62 100644 --- a/packages-private/diagnostics-frontend-browser/src/browser/controller.ts +++ b/packages-private/diagnostics-frontend/src/browser/controller.ts @@ -5,9 +5,9 @@ import { writeFile } from 'node:fs/promises'; import { chromium } from 'playwright'; -import type { Browser, BrowserContext, CDPSession, Page } from 'playwright'; import { enableNetworkTracking, type NetworkTracker } from './network'; -import type { BrowserDiagnostics, BrowserMeasurement, NetworkRequest, TabMemory, WebSocketConnection } from '../types'; +import type { Browser, BrowserContext, CDPSession, Page } from 'playwright'; +import type { BrowserDiagnostics, BrowserMeasurement, NetworkRequest, TabMemory, WebSocketConnection } from './types'; export type HeadlessChromeOptions = { scenarioTimeoutMs: number; diff --git a/packages-private/diagnostics-frontend-browser/src/browser/diagnostics.ts b/packages-private/diagnostics-frontend/src/browser/diagnostics.ts similarity index 92% rename from packages-private/diagnostics-frontend-browser/src/browser/diagnostics.ts rename to packages-private/diagnostics-frontend/src/browser/diagnostics.ts index 8365038bb2..a4c95bf842 100644 --- a/packages-private/diagnostics-frontend-browser/src/browser/diagnostics.ts +++ b/packages-private/diagnostics-frontend/src/browser/diagnostics.ts @@ -4,7 +4,7 @@ */ import { median } from 'diagnostics-shared/stats'; -import type { BrowserDiagnostics } from '../types'; +import type { BrowserDiagnostics } from './types'; export function summarizeBrowserDiagnostics(samples: BrowserDiagnostics[]): BrowserDiagnostics { const medianOf = (select: (sample: BrowserDiagnostics) => number) => median(samples.map(select)); diff --git a/packages-private/diagnostics-frontend-browser/src/browser/network.ts b/packages-private/diagnostics-frontend/src/browser/network.ts similarity index 99% rename from packages-private/diagnostics-frontend-browser/src/browser/network.ts rename to packages-private/diagnostics-frontend/src/browser/network.ts index 538294ec3d..77095ca689 100644 --- a/packages-private/diagnostics-frontend-browser/src/browser/network.ts +++ b/packages-private/diagnostics-frontend/src/browser/network.ts @@ -4,7 +4,7 @@ */ import type { CDPSession } from 'playwright'; -import type { NetworkRequest, NetworkSummary, WebSocketConnection } from '../types'; +import type { NetworkRequest, NetworkSummary, WebSocketConnection } from './types'; function normalizeHeaders(headers: Record | undefined) { if (headers == null) return undefined; diff --git a/packages-private/diagnostics-frontend-browser/src/report/html-styles.ts b/packages-private/diagnostics-frontend/src/browser/report/html-styles.ts similarity index 100% rename from packages-private/diagnostics-frontend-browser/src/report/html-styles.ts rename to packages-private/diagnostics-frontend/src/browser/report/html-styles.ts diff --git a/packages-private/diagnostics-frontend-browser/src/report/html.ts b/packages-private/diagnostics-frontend/src/browser/report/html.ts similarity index 98% rename from packages-private/diagnostics-frontend-browser/src/report/html.ts rename to packages-private/diagnostics-frontend/src/browser/report/html.ts index 7627f99729..ff1ca47839 100644 --- a/packages-private/diagnostics-frontend-browser/src/report/html.ts +++ b/packages-private/diagnostics-frontend/src/browser/report/html.ts @@ -224,7 +224,7 @@ function renderRound(round: number, diffs: RequestDiff[]): Raw { `; } -export function renderHtml(base: BrowserMetricsReport, head: BrowserMetricsReport) { +export function renderBrowserDiagnosticsHtml(base: BrowserMetricsReport, head: BrowserMetricsReport) { const diffs = diffReports(base, head); const rounds = [...new Set(diffs.map(diff => diff.round))].toSorted((a, b) => a - b); const generatedAt = new Date().toISOString(); diff --git a/packages-private/diagnostics-frontend-browser/src/scenario.ts b/packages-private/diagnostics-frontend/src/browser/scenario.ts similarity index 87% rename from packages-private/diagnostics-frontend-browser/src/scenario.ts rename to packages-private/diagnostics-frontend/src/browser/scenario.ts index 1ad513e93e..80df8532ec 100644 --- a/packages-private/diagnostics-frontend-browser/src/scenario.ts +++ b/packages-private/diagnostics-frontend/src/browser/scenario.ts @@ -3,9 +3,9 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { closeUserSetupDialog, postNote, registerUser, resetState, signupThroughUi, visitHome } from '../../../packages/frontend/test/e2e/shared'; +import { closeUserSetupDialog, postNote, registerUser, resetState, signupThroughUi, visitHome } from '../../../../packages/frontend/test/e2e/shared'; import { sleep } from './server'; -import type { HeadlessChromeController } from './browser/controller'; +import type { HeadlessChromeController } from './controller'; export const scenarioDescription = 'fresh browser signup, first timeline note, after the note becomes visible'; diff --git a/packages-private/diagnostics-frontend-browser/src/server.ts b/packages-private/diagnostics-frontend/src/browser/server.ts similarity index 90% rename from packages-private/diagnostics-frontend-browser/src/server.ts rename to packages-private/diagnostics-frontend/src/browser/server.ts index c2690c5777..72f7a6f79e 100644 --- a/packages-private/diagnostics-frontend-browser/src/server.ts +++ b/packages-private/diagnostics-frontend/src/browser/server.ts @@ -34,10 +34,15 @@ export function startServer(label: string, repoDir: string) { const serverStartupTimeoutMs = 120_000; +function hasExited(child: ChildProcess) { + return child.exitCode != null || child.signalCode != null; +} + export async function waitForServer(baseUrl: string, child: ChildProcess) { const startedAt = Date.now(); while (Date.now() - startedAt < serverStartupTimeoutMs) { if (child.exitCode != null) throw new Error(`Misskey server exited early with code ${child.exitCode}`); + if (child.signalCode != null) throw new Error(`Misskey server exited early with signal ${child.signalCode}`); try { // 応答が返らないままだとfetchが待ち続け、外側の120秒の上限を超えてしまう const remainingMs = serverStartupTimeoutMs - (Date.now() - startedAt); @@ -55,7 +60,7 @@ export async function waitForServer(baseUrl: string, child: ChildProcess) { } export async function stopServer(child: ChildProcess) { - if (child.exitCode != null) return; + if (hasExited(child)) return; if (process.platform === 'win32') { spawnSync('taskkill', ['/pid', String(child.pid), '/t', '/f'], { stdio: 'ignore' }); @@ -69,14 +74,14 @@ export async function stopServer(child: ChildProcess) { } await new Promise(resolvePromise => { - if (child.exitCode != null) { + if (hasExited(child)) { resolvePromise(); return; } const forceKillTimer = setTimeout(() => { // 猶予の間に終了していれば、PIDが再利用されて無関係のプロセスを撃つ恐れがある - if (child.exitCode == null && child.pid != null) { + if (!hasExited(child) && child.pid != null) { try { if (process.platform === 'win32') { spawnSync('taskkill', ['/pid', String(child.pid), '/t', '/f'], { stdio: 'ignore' }); diff --git a/packages-private/diagnostics-frontend-browser/src/summarize.ts b/packages-private/diagnostics-frontend/src/browser/summarize.ts similarity index 98% rename from packages-private/diagnostics-frontend-browser/src/summarize.ts rename to packages-private/diagnostics-frontend/src/browser/summarize.ts index 825ae188f5..1b88002208 100644 --- a/packages-private/diagnostics-frontend-browser/src/summarize.ts +++ b/packages-private/diagnostics-frontend/src/browser/summarize.ts @@ -5,7 +5,7 @@ import { finiteMedian } from 'diagnostics-shared/stats'; import { summarizeHeapSnapshotDataSamples } from 'diagnostics-shared/heap-snapshot'; -import { summarizeBrowserDiagnostics } from './browser/diagnostics'; +import { summarizeBrowserDiagnostics } from './diagnostics'; import type { BrowserMeasurement, BrowserMeasurementSample, BrowserMetricsReport, NetworkSummary } from './types'; export type SummarizeOptions = { diff --git a/packages-private/diagnostics-frontend-browser/src/types.ts b/packages-private/diagnostics-frontend/src/browser/types.ts similarity index 87% rename from packages-private/diagnostics-frontend-browser/src/types.ts rename to packages-private/diagnostics-frontend/src/browser/types.ts index df91071c13..78e31b76f4 100644 --- a/packages-private/diagnostics-frontend-browser/src/types.ts +++ b/packages-private/diagnostics-frontend/src/browser/types.ts @@ -5,6 +5,11 @@ import type { HeapSnapshotData } from 'diagnostics-shared/heap-snapshot'; +// NOTE: (AI向け) +// NetworkRequest がリクエスト本文と要求・応答ヘッダを保持し、APIキーやパスワード、Cookieなどの認証情報も含まれ得るが、 +// workflow上で作られるMisskeyアカウントはテスト用アカウントであり、誰かが実際に使用するプライベートなアカウントではないため、 +// それらの情報が第三者から閲覧できても何の問題もなく、脆弱性ではない。 + export type NetworkRequest = { requestId: string; url: string; diff --git a/packages-private/diagnostics-frontend-bundle/src/chunk-report.ts b/packages-private/diagnostics-frontend/src/bundle/chunk-report.ts similarity index 50% rename from packages-private/diagnostics-frontend-bundle/src/chunk-report.ts rename to packages-private/diagnostics-frontend/src/bundle/chunk-report.ts index 2b2492377c..55857305c5 100644 --- a/packages-private/diagnostics-frontend-bundle/src/chunk-report.ts +++ b/packages-private/diagnostics-frontend/src/bundle/chunk-report.ts @@ -9,7 +9,7 @@ import { escapeMdTableCell, formatBytes, } from 'diagnostics-shared/format'; -import type { CollectedReport, FileEntry } from './manifest'; +import type { CollectedBundleReport, FileEntry } from './manifest'; /** * この差分以下のチャンクは個別に出さず `(other)` にまとめる。 @@ -25,21 +25,21 @@ function entryDisplayName(entry: FileEntry | undefined) { return entry.displayName || entry.file; } -export function getChunkComparisonRows(keys: string[], before: Partial>, after: Partial>) { +export function getChunkComparisonRows(keys: string[], base: Partial>, head: Partial>) { return keys.map(key => { - const beforeEntry = before[key]; - const afterEntry = after[key]; - const beforeSize = beforeEntry?.size ?? 0; - const afterSize = afterEntry?.size ?? 0; + const baseEntry = base[key]; + const headEntry = head[key]; + const baseSize = baseEntry?.size ?? 0; + const headSize = headEntry?.size ?? 0; return { key, - name: entryDisplayName(beforeEntry ?? afterEntry), - beforeFile: beforeEntry?.file, - afterFile: afterEntry?.file, - beforeSize, - afterSize, - changeType: beforeEntry == null ? 'added' : afterEntry == null ? 'removed' : beforeSize !== afterSize ? 'updated' : 'unchanged', - sortSize: Math.max(beforeSize, afterSize), + name: entryDisplayName(baseEntry ?? headEntry), + baseFile: baseEntry?.file, + headFile: headEntry?.file, + baseSize, + headSize, + changeType: baseEntry == null ? 'added' : headEntry == null ? 'removed' : baseSize !== headSize ? 'updated' : 'unchanged', + sortSize: Math.max(baseSize, headSize), }; }); } @@ -47,10 +47,10 @@ export function getChunkComparisonRows(keys: string[], before: Partial[number]; export type ChunkAggregate = { - beforeSize: number; - afterSize: number; - beforeCount: number; - afterCount: number; + baseSize: number; + headSize: number; + baseCount: number; + headCount: number; }; export function sumChunkSizes(chunks: FileEntry[]) { @@ -58,29 +58,29 @@ export function sumChunkSizes(chunks: FileEntry[]) { } /** - * 比較キーを持たない (= before/after で対応付けできない) チャンクの合計。 + * 比較キーを持たない (= base/head で対応付けできない) チャンクの合計。 */ -export function generatedAggregate(before: FileEntry[], after: FileEntry[]): ChunkAggregate { - const beforeGenerated = before.filter(chunk => chunk.comparisonKey == null); - const afterGenerated = after.filter(chunk => chunk.comparisonKey == null); +export function generatedAggregate(base: FileEntry[], head: FileEntry[]): ChunkAggregate { + const baseGenerated = base.filter(chunk => chunk.comparisonKey == null); + const headGenerated = head.filter(chunk => chunk.comparisonKey == null); return { - beforeSize: sumChunkSizes(beforeGenerated), - afterSize: sumChunkSizes(afterGenerated), - beforeCount: beforeGenerated.length, - afterCount: afterGenerated.length, + baseSize: sumChunkSizes(baseGenerated), + headSize: sumChunkSizes(headGenerated), + baseCount: baseGenerated.length, + headCount: headGenerated.length, }; } export function hasSmallDelta(row: ChunkComparisonRow) { - return Math.abs(row.afterSize - row.beforeSize) <= smallDeltaThreshold; + return Math.abs(row.headSize - row.baseSize) <= smallDeltaThreshold; } export function comparisonRowsAggregate(rows: ChunkComparisonRow[]): ChunkAggregate { return { - beforeSize: rows.reduce((sum, row) => sum + row.beforeSize, 0), - afterSize: rows.reduce((sum, row) => sum + row.afterSize, 0), - beforeCount: rows.filter(row => row.beforeFile != null).length, - afterCount: rows.filter(row => row.afterFile != null).length, + baseSize: rows.reduce((sum, row) => sum + row.baseSize, 0), + headSize: rows.reduce((sum, row) => sum + row.headSize, 0), + baseCount: rows.filter(row => row.baseFile != null).length, + headCount: rows.filter(row => row.headFile != null).length, }; } @@ -108,69 +108,69 @@ export function formatChunkChangeSummary(label: string, summary: ReturnType 0 || generated.afterCount > 0); - const hasOther = other != null && (other.beforeCount > 0 || other.afterCount > 0); + const hasGenerated = generated != null && (generated.baseCount > 0 || generated.headCount > 0); + const hasOther = other != null && (other.baseCount > 0 || other.headCount > 0); if (rows.length === 0 && total == null && !hasGenerated && !hasOther) return '_No data_'; const lines = [ - '| Chunk | Before | After | Δ | Δ (%) |', + '| Chunk | Base | Head | Δ | Δ (%) |', '| --- | ---: | ---: | ---: | ---: |', ]; if (total != null) { - lines.push(`| (total) | ${formatBytes(total.beforeSize)} | ${formatBytes(total.afterSize)} | ${calcAndFormatDeltaBytes(total.beforeSize, total.afterSize, 1000)} | ${calcAndFormatDeltaPercentInMdTable(total.beforeSize, total.afterSize, 0.1)} |`); + lines.push(`| (total) | ${formatBytes(total.baseSize)} | ${formatBytes(total.headSize)} | ${calcAndFormatDeltaBytes(total.baseSize, total.headSize, 1000)} | ${calcAndFormatDeltaPercentInMdTable(total.baseSize, total.headSize, 0.1)} |`); lines.push('| | | | | |'); } for (const row of rows) { const chunkFile = chunkFileDisplay(row); if (row.changeType === 'added') { - lines.push(`|
    \`${escapeMdTableCell(row.name)}\` \`${escapeMdTableCell(chunkFile)}\`
    | ${formatBytes(row.beforeSize)} | ${formatBytes(row.afterSize)} | ${calcAndFormatDeltaBytes(row.beforeSize, row.afterSize, 1000)} | $\\color{orange}{\\text{( + )}}$ |`); + lines.push(`|
    \`${escapeMdTableCell(row.name)}\` \`${escapeMdTableCell(chunkFile)}\`
    | ${formatBytes(row.baseSize)} | ${formatBytes(row.headSize)} | ${calcAndFormatDeltaBytes(row.baseSize, row.headSize, 1000)} | $\\color{orange}{\\text{( + )}}$ |`); } else if (row.changeType === 'removed') { - lines.push(`|
    \`${escapeMdTableCell(row.name)}\` \`${escapeMdTableCell(chunkFile)}\`
    | ${formatBytes(row.beforeSize)} | ${formatBytes(row.afterSize)} | ${calcAndFormatDeltaBytes(row.beforeSize, row.afterSize, 1000)} | $\\color{green}{\\text{( - )}}$ |`); + lines.push(`|
    \`${escapeMdTableCell(row.name)}\` \`${escapeMdTableCell(chunkFile)}\`
    | ${formatBytes(row.baseSize)} | ${formatBytes(row.headSize)} | ${calcAndFormatDeltaBytes(row.baseSize, row.headSize, 1000)} | $\\color{green}{\\text{( - )}}$ |`); } else { - lines.push(`|
    \`${escapeMdTableCell(row.name)}\` \`${escapeMdTableCell(chunkFile)}\`
    | ${formatBytes(row.beforeSize)} | ${formatBytes(row.afterSize)} | ${calcAndFormatDeltaBytes(row.beforeSize, row.afterSize, 1000)} | ${calcAndFormatDeltaPercentInMdTable(row.beforeSize, row.afterSize, 0.1)} |`); + lines.push(`|
    \`${escapeMdTableCell(row.name)}\` \`${escapeMdTableCell(chunkFile)}\`
    | ${formatBytes(row.baseSize)} | ${formatBytes(row.headSize)} | ${calcAndFormatDeltaBytes(row.baseSize, row.headSize, 1000)} | ${calcAndFormatDeltaPercentInMdTable(row.baseSize, row.headSize, 0.1)} |`); } } if (hasGenerated) { - lines.push(`| (other generated chunks) | ${formatBytes(generated.beforeSize)} | ${formatBytes(generated.afterSize)} | ${calcAndFormatDeltaBytes(generated.beforeSize, generated.afterSize, 1000)} | ${calcAndFormatDeltaPercentInMdTable(generated.beforeSize, generated.afterSize, 0.1)} |`); + lines.push(`| (other generated chunks) | ${formatBytes(generated.baseSize)} | ${formatBytes(generated.headSize)} | ${calcAndFormatDeltaBytes(generated.baseSize, generated.headSize, 1000)} | ${calcAndFormatDeltaPercentInMdTable(generated.baseSize, generated.headSize, 0.1)} |`); } if (hasOther) { - lines.push(`| (other) | ${formatBytes(other.beforeSize)} | ${formatBytes(other.afterSize)} | ${calcAndFormatDeltaBytes(other.beforeSize, other.afterSize, 1000)} | ${calcAndFormatDeltaPercentInMdTable(other.beforeSize, other.afterSize, 0.1)} |`); + lines.push(`| (other) | ${formatBytes(other.baseSize)} | ${formatBytes(other.headSize)} | ${calcAndFormatDeltaBytes(other.baseSize, other.headSize, 1000)} | ${calcAndFormatDeltaPercentInMdTable(other.baseSize, other.headSize, 0.1)} |`); } return lines.join('\n'); } -export function renderFrontendChunkReport(before: CollectedReport, after: CollectedReport) { - const beforeComparable = before.comparableChunks; - const afterComparable = after.comparableChunks; - const allChunkKeys = [...new Set([...Object.keys(beforeComparable), ...Object.keys(afterComparable)])]; - const allComparisonRows = getChunkComparisonRows(allChunkKeys, beforeComparable, afterComparable); +export function renderFrontendChunkReport(base: CollectedBundleReport, head: CollectedBundleReport) { + const baseComparable = base.comparableChunks; + const headComparable = head.comparableChunks; + const allChunkKeys = [...new Set([...Object.keys(baseComparable), ...Object.keys(headComparable)])]; + const allComparisonRows = getChunkComparisonRows(allChunkKeys, baseComparable, headComparable); const changedRows = allComparisonRows.filter((row) => row.changeType !== 'unchanged'); const diffSummary = summarizeChunkChanges(changedRows); const diffTotal = { - beforeSize: sumChunkSizes(before.chunks), - afterSize: sumChunkSizes(after.chunks), + baseSize: sumChunkSizes(base.chunks), + headSize: sumChunkSizes(head.chunks), }; - const diffGenerated = generatedAggregate(before.chunks, after.chunks); + const diffGenerated = generatedAggregate(base.chunks, head.chunks); const largeDeltaRows = changedRows.filter(row => !hasSmallDelta(row)).sort(compareChunkComparisonRows); const diffRows = largeDeltaRows.slice(0, diffRowLimit); // 表示上限で切り捨てた行も `(other)` に含める。落とすと合計が実際の変化量と合わなくなる @@ -179,22 +179,22 @@ export function renderFrontendChunkReport(before: CollectedReport, after: Collec ...largeDeltaRows.slice(diffRowLimit), ]); - const beforeStartupFiles = new Set(before.startupFiles); - const afterStartupFiles = new Set(after.startupFiles); - const beforeStartupChunks = before.chunks.filter(chunk => beforeStartupFiles.has(chunk.file)); - const afterStartupChunks = after.chunks.filter(chunk => afterStartupFiles.has(chunk.file)); - const beforeStartupComparable = comparableMap(beforeStartupChunks); - const afterStartupComparable = comparableMap(afterStartupChunks); - const startupKeys = [...new Set([...Object.keys(beforeStartupComparable), ...Object.keys(afterStartupComparable)])]; - const startupComparisonRows = getChunkComparisonRows(startupKeys, beforeStartupComparable, afterStartupComparable); + const baseStartupFiles = new Set(base.startupFiles); + const headStartupFiles = new Set(head.startupFiles); + const baseStartupChunks = base.chunks.filter(chunk => baseStartupFiles.has(chunk.file)); + const headStartupChunks = head.chunks.filter(chunk => headStartupFiles.has(chunk.file)); + const baseStartupComparable = comparableMap(baseStartupChunks); + const headStartupComparable = comparableMap(headStartupChunks); + const startupKeys = [...new Set([...Object.keys(baseStartupComparable), ...Object.keys(headStartupComparable)])]; + const startupComparisonRows = getChunkComparisonRows(startupKeys, baseStartupComparable, headStartupComparable); const startupSummary = summarizeChunkChanges(startupComparisonRows); const startupOther = comparisonRowsAggregate(startupComparisonRows.filter(hasSmallDelta)); const startupRows = startupComparisonRows.filter(row => !hasSmallDelta(row)).sort(compareChunkComparisonRows); const startupTotal = { - beforeSize: sumChunkSizes(beforeStartupChunks), - afterSize: sumChunkSizes(afterStartupChunks), + baseSize: sumChunkSizes(baseStartupChunks), + headSize: sumChunkSizes(headStartupChunks), }; - const startupGenerated = generatedAggregate(beforeStartupChunks, afterStartupChunks); + const startupGenerated = generatedAggregate(baseStartupChunks, headStartupChunks); return [ '
    ', diff --git a/packages-private/diagnostics-frontend-bundle/src/fs-utils.ts b/packages-private/diagnostics-frontend/src/bundle/fs-utils.ts similarity index 90% rename from packages-private/diagnostics-frontend-bundle/src/fs-utils.ts rename to packages-private/diagnostics-frontend/src/bundle/fs-utils.ts index 74ed757b53..c6b1d3d2e6 100644 --- a/packages-private/diagnostics-frontend-bundle/src/fs-utils.ts +++ b/packages-private/diagnostics-frontend/src/bundle/fs-utils.ts @@ -18,8 +18,9 @@ export async function fileExists(filePath: string) { try { await fs.access(filePath); return true; - } catch { - return false; + } catch (error) { + if ((error as NodeJS.ErrnoException).code === 'ENOENT') return false; + throw error; } } diff --git a/packages-private/diagnostics-frontend-bundle/src/manifest.ts b/packages-private/diagnostics-frontend/src/bundle/manifest.ts similarity index 90% rename from packages-private/diagnostics-frontend-bundle/src/manifest.ts rename to packages-private/diagnostics-frontend/src/bundle/manifest.ts index d35a5cfbfb..05b00c2a09 100644 --- a/packages-private/diagnostics-frontend-bundle/src/manifest.ts +++ b/packages-private/diagnostics-frontend/src/bundle/manifest.ts @@ -6,7 +6,16 @@ import { promises as fs } from 'node:fs'; import path from 'node:path'; import { fileExists, fileSize, normalizePath, traverseDirectory } from './fs-utils'; -import type { Manifest, ManifestChunk } from 'vite'; + +export type BundleManifestChunk = { + file: string; + src?: string; + name?: string; + isEntry?: boolean; + imports?: string[]; +}; + +export type BundleManifest = Record; /** * 比較対象とするロケール。ロケール別チャンクは全ロケール分だと数が多すぎるため、 @@ -27,15 +36,15 @@ export type FileEntry = { size: number; }; -export type CollectedReport = { - manifest: Manifest; +export type CollectedBundleReport = { + manifest: BundleManifest; chunks: FileEntry[]; comparableChunks: Record; chunksByManifestKey: Record; startupFiles: string[]; }; -export function findEntryKey(manifest: Manifest) { +export function findEntryKey(manifest: BundleManifest) { const entries = Object.entries(manifest); return entries.find(([key, chunk]) => key === 'src/_boot_.ts' || chunk.src === 'src/_boot_.ts')?.[0] ?? entries.find(([, chunk]) => chunk.name === 'entry' && chunk.isEntry)?.[0] @@ -45,9 +54,9 @@ export function findEntryKey(manifest: Manifest) { /** * ビルド間で安定するチャンク識別子。出力ファイル名はハッシュ付きで毎回変わるため、 - * これが取れないチャンクは before/after の対応付けができない。 + * これが取れないチャンクは base/head の対応付けができない。 */ -export function stableChunkKey(chunk: ManifestChunk) { +export function stableChunkKey(chunk: BundleManifestChunk) { if (chunk.src != null) return `src:${normalizePath(chunk.src)}`; if (chunk.name != null && stableNamedChunks.has(chunk.name)) return `named:${chunk.name}`; return null; @@ -56,7 +65,7 @@ export function stableChunkKey(chunk: ManifestChunk) { /** * 起動時に必ず読み込まれるチャンク (entry とその静的 import) の manifest キーを集める。 */ -export function collectStartupManifestKeys(manifest: Manifest) { +export function collectStartupManifestKeys(manifest: BundleManifest) { const entryKey = findEntryKey(manifest); const keys = new Set(); if (entryKey == null) throw new Error('Unable to find frontend startup entry in Vite manifest.'); @@ -101,10 +110,10 @@ export async function resolveBuiltFile(outDir: string, file: string) { }; } -export async function collectReport(repoDir: string): Promise { +export async function collectBundleReport(repoDir: string): Promise { const outDir = path.join(repoDir, 'built/_frontend_vite_'); const manifestPath = path.join(outDir, 'manifest.json'); - const manifest = JSON.parse(await fs.readFile(manifestPath, 'utf8')) as Manifest; + const manifest = JSON.parse(await fs.readFile(manifestPath, 'utf8')) as BundleManifest; const chunksByFile = new Map(); const comparableChunks = new Map(); const chunksByManifestKey = new Map(); diff --git a/packages-private/diagnostics-frontend-bundle/src/visualizer.ts b/packages-private/diagnostics-frontend/src/bundle/visualizer.ts similarity index 83% rename from packages-private/diagnostics-frontend-bundle/src/visualizer.ts rename to packages-private/diagnostics-frontend/src/bundle/visualizer.ts index 17e4f190b6..199accf2d7 100644 --- a/packages-private/diagnostics-frontend-bundle/src/visualizer.ts +++ b/packages-private/diagnostics-frontend/src/bundle/visualizer.ts @@ -141,7 +141,7 @@ export function collectVisualizerReport(data: VisualizerReport) { * 要素として配列のまま埋め込まれていたため、テーブルがカンマ区切りの1行に潰れていた。 * 呼び出し側で意識しなくて済むよう、ここで文字列にして返す。 */ -export function renderVisualizerSummaryTable(before: ReturnType, after: ReturnType) { +export function renderVisualizerSummaryTable(base: ReturnType, head: ReturnType) { const summary = [ 'bundles', 'modules', @@ -178,25 +178,25 @@ export function renderVisualizerSummaryTable(before: ReturnType', '', '', - 'Before', - ...summary.map((key) => `${formatNumber(before.summary[key])}`), - ...metrics.map((key) => `${formatBytes(before.metrics[key])}`), + 'Base', + ...summary.map((key) => `${formatNumber(base.summary[key])}`), + ...metrics.map((key) => `${formatBytes(base.metrics[key])}`), '', '', - 'After', - ...summary.map((key) => `${formatNumber(after.summary[key])}`), - ...metrics.map((key) => `${formatBytes(after.metrics[key])}`), + 'Head', + ...summary.map((key) => `${formatNumber(head.summary[key])}`), + ...metrics.map((key) => `${formatBytes(head.metrics[key])}`), '', '', '', 'Δ', - ...summary.map((key) => `${calcAndFormatDeltaNumber(before.summary[key], after.summary[key], 0)}`), - ...metrics.map((key) => `${calcAndFormatDeltaBytes(before.metrics[key], after.metrics[key], 1000)}`), + ...summary.map((key) => `${calcAndFormatDeltaNumber(base.summary[key], head.summary[key], 0)}`), + ...metrics.map((key) => `${calcAndFormatDeltaBytes(base.metrics[key], head.metrics[key], 1000)}`), '', '', 'Δ (%)', - ...summary.map((key) => `${calcAndFormatDeltaPercent(before.summary[key], after.summary[key], 0.1)}`), - ...metrics.map((key) => `${calcAndFormatDeltaPercent(before.metrics[key], after.metrics[key], 0.1)}`), + ...summary.map((key) => `${calcAndFormatDeltaPercent(base.summary[key], head.summary[key], 0.1)}`), + ...metrics.map((key) => `${calcAndFormatDeltaPercent(base.metrics[key], head.metrics[key], 0.1)}`), '', '', '', diff --git a/packages-private/diagnostics-frontend-browser/src/inspect.ts b/packages-private/diagnostics-frontend/src/inspect-browser.ts similarity index 95% rename from packages-private/diagnostics-frontend-browser/src/inspect.ts rename to packages-private/diagnostics-frontend/src/inspect-browser.ts index 9cf08664e9..25d2703e99 100644 --- a/packages-private/diagnostics-frontend-browser/src/inspect.ts +++ b/packages-private/diagnostics-frontend/src/inspect-browser.ts @@ -9,10 +9,10 @@ import { readIntegerEnv, readOptionalEnv } from 'diagnostics-shared/env'; import { analyzeHeapSnapshot, defaultHeapSnapshotBreakdownTopN } from 'diagnostics-shared/heap-snapshot'; import { HeadlessChromeController } from './browser/controller'; import { summarizeNetwork } from './browser/network'; -import { prepareInstance, runSignupAndPostScenario, scenarioDescription } from './scenario'; -import { startServer, stopServer, waitForServer } from './server'; -import { selectRepresentativeSample, summarizeSamples } from './summarize'; -import type { BrowserMeasurementSample, BrowserMetricsReport } from './types'; +import { prepareInstance, runSignupAndPostScenario, scenarioDescription } from './browser/scenario'; +import { startServer, stopServer, waitForServer } from './browser/server'; +import { selectRepresentativeSample, summarizeSamples } from './browser/summarize'; +import type { BrowserMeasurementSample, BrowserMetricsReport } from './browser/types'; type Label = 'base' | 'head'; @@ -110,7 +110,7 @@ async function genReport(label: Label, repoDir: string, outputPath: string) { async function main() { const [baseDirArg, headDirArg, baseOutputArg, headOutputArg] = process.argv.slice(2); if (baseDirArg == null || headDirArg == null || baseOutputArg == null || headOutputArg == null) { - throw new Error('Usage: inspect '); + throw new Error('Usage: inspect-browser '); } for (const label of labels) { diff --git a/packages-private/diagnostics-frontend-browser/src/render-html.ts b/packages-private/diagnostics-frontend/src/render-browser-html.ts similarity index 68% rename from packages-private/diagnostics-frontend-browser/src/render-html.ts rename to packages-private/diagnostics-frontend/src/render-browser-html.ts index d3ed90927a..c6567e088e 100644 --- a/packages-private/diagnostics-frontend-browser/src/render-html.ts +++ b/packages-private/diagnostics-frontend/src/render-browser-html.ts @@ -5,19 +5,19 @@ import { readFile, writeFile } from 'node:fs/promises'; import { resolve } from 'node:path'; -import { renderHtml } from './report/html'; -import type { BrowserMetricsReport } from './types'; +import { renderBrowserDiagnosticsHtml } from './browser/report/html'; +import type { BrowserMetricsReport } from './browser/types'; async function main() { const [baseFileArg, headFileArg, outputFileArg] = process.argv.slice(2); if (baseFileArg == null || headFileArg == null || outputFileArg == null) { - throw new Error('Usage: render-html '); + throw new Error('Usage: render-browser-html '); } const base = JSON.parse(await readFile(resolve(baseFileArg), 'utf8')) as BrowserMetricsReport; const head = JSON.parse(await readFile(resolve(headFileArg), 'utf8')) as BrowserMetricsReport; - await writeFile(resolve(outputFileArg), renderHtml(base, head)); + await writeFile(resolve(outputFileArg), renderBrowserDiagnosticsHtml(base, head)); } await main().catch(err => { diff --git a/packages-private/diagnostics-frontend/src/render-md.ts b/packages-private/diagnostics-frontend/src/render-md.ts new file mode 100644 index 0000000000..a9cfbc1cd1 --- /dev/null +++ b/packages-private/diagnostics-frontend/src/render-md.ts @@ -0,0 +1,62 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { readFile, writeFile } from 'node:fs/promises'; +import { resolve } from 'node:path'; +import { readOptionalEnv, readRequiredEnv } from 'diagnostics-shared/env'; +import { collectBundleReport } from './bundle/manifest'; +import { renderFrontendDiagnosticsMarkdown } from './report'; +import type { BrowserMetricsReport } from './browser/types'; +import type { VisualizerReport } from './bundle/visualizer'; + +const args = process.argv.slice(2); +if (args.length !== 7) { + throw new Error('Usage: render-md '); +} +const [ + baseDirArg, + headDirArg, + baseBundleStatsFileArg, + headBundleStatsFileArg, + baseBrowserFileArg, + headBrowserFileArg, + outputFileArg, +] = args as [string, string, string, string, string, string, string]; + +const [ + baseBundle, + headBundle, + baseBundleStatsJson, + headBundleStatsJson, + baseBrowserJson, + headBrowserJson, +] = await Promise.all([ + collectBundleReport(resolve(baseDirArg)), + collectBundleReport(resolve(headDirArg)), + readFile(resolve(baseBundleStatsFileArg), 'utf8'), + readFile(resolve(headBundleStatsFileArg), 'utf8'), + readFile(resolve(baseBrowserFileArg), 'utf8'), + readFile(resolve(headBrowserFileArg), 'utf8'), +]); + +await writeFile( + resolve(outputFileArg), + renderFrontendDiagnosticsMarkdown({ + bundle: { + base: baseBundle, + head: headBundle, + baseStats: JSON.parse(baseBundleStatsJson) as VisualizerReport, + headStats: JSON.parse(headBundleStatsJson) as VisualizerReport, + visualizerArtifactUrl: readRequiredEnv('FRONTEND_BUNDLE_REPORT_ARTIFACT_URL'), + }, + browser: { + base: JSON.parse(baseBrowserJson) as BrowserMetricsReport, + head: JSON.parse(headBrowserJson) as BrowserMetricsReport, + baseHeapSnapshotUrl: readRequiredEnv('FRONTEND_BROWSER_BASE_HEAP_SNAPSHOT_ARTIFACT_URL'), + headHeapSnapshotUrl: readRequiredEnv('FRONTEND_BROWSER_HEAD_HEAP_SNAPSHOT_ARTIFACT_URL'), + detailedHtmlUrl: readOptionalEnv('FRONTEND_BROWSER_DETAILED_HTML_ARTIFACT_URL'), + }, + }), +); diff --git a/packages-private/diagnostics-frontend-browser/src/report/markdown.ts b/packages-private/diagnostics-frontend/src/report.ts similarity index 86% rename from packages-private/diagnostics-frontend-browser/src/report/markdown.ts rename to packages-private/diagnostics-frontend/src/report.ts index 370cb86892..e605b0f931 100644 --- a/packages-private/diagnostics-frontend-browser/src/report/markdown.ts +++ b/packages-private/diagnostics-frontend/src/report.ts @@ -4,14 +4,29 @@ */ import { formatBytes, formatColoredDelta, formatNumber } from 'diagnostics-shared/format'; -import { pairedDeltaSummary, sampleSpread } from 'diagnostics-shared/stats'; import { renderHeapSnapshotTable, type HeapSnapshotReport } from 'diagnostics-shared/heap-snapshot'; -import type { BrowserMeasurement, BrowserMeasurementSample, BrowserMetricsReport } from '../types'; +import { pairedDeltaSummary } from 'diagnostics-shared/stats'; +import { renderFrontendChunkReport } from './bundle/chunk-report'; +import { collectVisualizerReport, renderVisualizerSummaryTable, type VisualizerReport } from './bundle/visualizer'; +import type { CollectedBundleReport } from './bundle/manifest'; +import type { BrowserMeasurement, BrowserMeasurementSample, BrowserMetricsReport } from './browser/types'; -export type RenderMarkdownOptions = { - baseHeapSnapshotUrl: string; - headHeapSnapshotUrl: string; - detailedHtmlUrl?: string | null; +export type FrontendDiagnosticsMarkdownInput = { + bundle: { + base: CollectedBundleReport; + head: CollectedBundleReport; + baseStats: VisualizerReport; + headStats: VisualizerReport; + /** rollup-plugin-visualizer が出力したtreemap HTMLのartifact URL */ + visualizerArtifactUrl: string; + }; + browser: { + base: BrowserMetricsReport; + head: BrowserMetricsReport; + baseHeapSnapshotUrl: string; + headHeapSnapshotUrl: string; + detailedHtmlUrl?: string | null; + }; }; function renderMetricRow( @@ -45,7 +60,7 @@ function resourceTypeSampleBytes(sample: BrowserMeasurementSample, resourceTypes return resourceTypeBytes(sample, resourceTypes); } -function renderSummaryTable(base: BrowserMetricsReport, head: BrowserMetricsReport, all = false) { +function renderBrowserSummaryTable(base: BrowserMetricsReport, head: BrowserMetricsReport, all = false) { //function getMetric(report: BrowserMeasurement, key: string) { // return report.performance.cdpMetrics[key]; //} @@ -154,13 +169,14 @@ function toHeapSnapshotReport(report: BrowserMetricsReport): HeapSnapshotReport }; } -export function renderMarkdown(base: BrowserMetricsReport, head: BrowserMetricsReport, options: RenderMarkdownOptions) { - const detailedHtmlUrl = options.detailedHtmlUrl; - const heapSnapshotTable = renderHeapSnapshotTable(toHeapSnapshotReport(base), toHeapSnapshotReport(head)); +export function renderFrontendDiagnosticsMarkdown(input: FrontendDiagnosticsMarkdownInput) { + const { bundle, browser } = input; + const detailedHtmlUrl = browser.detailedHtmlUrl; + const heapSnapshotTable = renderHeapSnapshotTable(toHeapSnapshotReport(browser.base), toHeapSnapshotReport(browser.head)); const lines = [ - '## 🖥 Frontend Browser Diagnostics Report', + '## 🖥 Frontend Diagnostics Report', '', - renderSummaryTable(base, head), + renderBrowserSummaryTable(browser.base, browser.head), '', 'Only metrics showing significant changes are displayed.', '', @@ -169,7 +185,7 @@ export function renderMarkdown(base: BrowserMetricsReport, head: BrowserMetricsR '
    ', 'Requests by resource type', '', - renderResourceTypeTable(base, head), + renderResourceTypeTable(browser.base, browser.head), '', '
    ', '', @@ -178,11 +194,19 @@ export function renderMarkdown(base: BrowserMetricsReport, head: BrowserMetricsR '', heapSnapshotTable ?? '_No V8 heap snapshot data._', '', - //renderHeapSnapshotSankey(toHeapSnapshotReport(head), 'Head'), + //renderHeapSnapshotSankey(toHeapSnapshotReport(browser.head), 'Head'), //'', - `Download representative heap snapshot: [base](${options.baseHeapSnapshotUrl}) / [head](${options.headHeapSnapshotUrl})`, + `Download representative heap snapshot: [base](${browser.baseHeapSnapshotUrl}) / [head](${browser.headHeapSnapshotUrl})`, '
    ', '', + '### 📦 Bundle Stats', + '', + renderFrontendChunkReport(bundle.base, bundle.head), + '', + renderVisualizerSummaryTable(collectVisualizerReport(bundle.baseStats), collectVisualizerReport(bundle.headStats)), + '', + `[Open treemap HTML](${bundle.visualizerArtifactUrl})`, + '', ]; return lines.filter(line => line != null).join('\n').trimEnd() + '\n'; diff --git a/packages-private/diagnostics-frontend-browser/test/__snapshots__/render-md.md b/packages-private/diagnostics-frontend/test/__snapshots__/report.md similarity index 62% rename from packages-private/diagnostics-frontend-browser/test/__snapshots__/render-md.md rename to packages-private/diagnostics-frontend/test/__snapshots__/report.md index 0f791057c2..41d3aae727 100644 --- a/packages-private/diagnostics-frontend-browser/test/__snapshots__/render-md.md +++ b/packages-private/diagnostics-frontend/test/__snapshots__/report.md @@ -1,4 +1,4 @@ -## 🖥 Frontend Browser Diagnostics Report +## 🖥 Frontend Diagnostics Report | Metric | Base | Head | Δ median | Δ MAD | Δ min | Δ max | | --- | ---: | ---: | ---: | ---: | ---: | ---: | @@ -81,3 +81,102 @@ Download representative heap snapshot: [base](https://example.invalid/base) / [head](https://example.invalid/head) + +### 📦 Bundle Stats + +
    +Chunk size diff (2 updated, 0 added, 0 removed) + +| Chunk | Base | Head | Δ | Δ (%) | +| --- | ---: | ---: | ---: | ---: | +| (total) | 120 KB | 127 KB | $\color{orange}{\text{+6.3 KB}}$ | $\color{orange}{\text{+5.2\\%}}$ | +| | | | | | +|
    `vue` `assets/vue-b2.js`
    | 90 KB | 96 KB | $\color{orange}{\text{+6 KB}}$ | $\color{orange}{\text{+6.7\\%}}$ | +| (other generated chunks) | 1.2 KB | 1.5 KB | $\text{+300 B}$ | $\color{orange}{\text{+25\\%}}$ | +| (other) | 20 KB | 20 KB | $\text{+3 B}$ | $\text{+0\\%}$ | + +
    + +
    +Startup chunk size (2 updated, 0 added, 0 removed) + +| Chunk | Base | Head | Δ | Δ (%) | +| --- | ---: | ---: | ---: | ---: | +| (total) | 114 KB | 120 KB | $\color{orange}{\text{+6 KB}}$ | $\color{orange}{\text{+5.3\\%}}$ | +| | | | | | +|
    `vue` `assets/vue-b2.js`
    | 90 KB | 96 KB | $\color{orange}{\text{+6 KB}}$ | $\color{orange}{\text{+6.7\\%}}$ | +| (other) | 24 KB | 24 KB | $\text{+3 B}$ | $\text{+0\\%}$ | + +_Startup chunks are the Vite entry for `src/_boot_.ts` and its static imports._ + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    BundlesModulesEntriesImportsSize
    StaticDynamicRenderedGzipBrotli
    Base2612321 KB6.3 KB5.3 KB
    Head2713331 KB8.4 KB7 KB
    Δ0$\color{orange}{\text{+1}}$0$\color{orange}{\text{+1}}$0$\color{orange}{\text{+9.8 KB}}$$\color{orange}{\text{+2.1 KB}}$$\color{orange}{\text{+1.8 KB}}$
    Δ (%)0%$\color{orange}{\text{+16.7\%}}$0%$\color{orange}{\text{+50\%}}$0%$\color{orange}{\text{+46.7\%}}$$\color{orange}{\text{+33.3\%}}$$\color{orange}{\text{+33.3\%}}$
    + +[Open treemap HTML](https://example.invalid/treemap) diff --git a/packages-private/diagnostics-frontend-browser/test/__snapshots__/render-html.html b/packages-private/diagnostics-frontend/test/browser/__snapshots__/render-html.html similarity index 100% rename from packages-private/diagnostics-frontend-browser/test/__snapshots__/render-html.html rename to packages-private/diagnostics-frontend/test/browser/__snapshots__/render-html.html diff --git a/packages-private/diagnostics-frontend-browser/test/fixtures/base.json b/packages-private/diagnostics-frontend/test/browser/fixtures/base.json similarity index 100% rename from packages-private/diagnostics-frontend-browser/test/fixtures/base.json rename to packages-private/diagnostics-frontend/test/browser/fixtures/base.json diff --git a/packages-private/diagnostics-frontend-browser/test/fixtures/head.json b/packages-private/diagnostics-frontend/test/browser/fixtures/head.json similarity index 100% rename from packages-private/diagnostics-frontend-browser/test/fixtures/head.json rename to packages-private/diagnostics-frontend/test/browser/fixtures/head.json diff --git a/packages-private/diagnostics-frontend/test/browser/render.test.ts b/packages-private/diagnostics-frontend/test/browser/render.test.ts new file mode 100644 index 0000000000..9def362ff6 --- /dev/null +++ b/packages-private/diagnostics-frontend/test/browser/render.test.ts @@ -0,0 +1,44 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; +import { afterEach, beforeEach, expect, test, vi } from 'vitest'; +import { renderBrowserDiagnosticsHtml } from '../../src/browser/report/html'; +import type { BrowserMetricsReport } from '../../src/browser/types'; + +const fixturesDir = join(import.meta.dirname, 'fixtures'); + +async function loadFixture(name: string) { + return JSON.parse(await readFile(join(fixturesDir, `${name}.json`), 'utf8')) as BrowserMetricsReport; +} + +beforeEach(() => { + // renderBrowserDiagnosticsHtml() が生成時刻を埋め込むので、スナップショットが揺れないよう時刻を固定する + vi.useFakeTimers(); + vi.setSystemTime(new Date('2026-07-18T00:00:00.000Z')); +}); + +afterEach(() => { + vi.useRealTimers(); +}); + +test('renders the network request diff html report', async () => { + const html = renderBrowserDiagnosticsHtml(await loadFixture('base'), await loadFixture('head')); + + await expect(html).toMatchFileSnapshot('./__snapshots__/render-html.html'); +}); + +test('escapes html metacharacters coming from the browser session', async () => { + const base = await loadFixture('base'); + const head = await loadFixture('head'); + // CDP由来の値は原理的には任意の文字列になりうるので、生HTMLとして出ないことを確かめる + head.samples[0].networkRequests[0].url = 'http://127.0.0.1:61812/">'; + + const html = renderBrowserDiagnosticsHtml(base, head); + + expect(html).not.toContain(''); + expect(html).toContain('<script>alert(1)</script>'); +}); diff --git a/packages-private/diagnostics-frontend/test/browser/server.test.ts b/packages-private/diagnostics-frontend/test/browser/server.test.ts new file mode 100644 index 0000000000..24f3e62444 --- /dev/null +++ b/packages-private/diagnostics-frontend/test/browser/server.test.ts @@ -0,0 +1,47 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { EventEmitter } from 'node:events'; +import { afterEach, expect, test, vi } from 'vitest'; +import { stopServer, waitForServer } from '../../src/browser/server'; +import type { ChildProcess } from 'node:child_process'; + +vi.mock('node:child_process', async () => { + const actual = await vi.importActual('node:child_process'); + return { + ...actual, + spawnSync: vi.fn(), + }; +}); + +function signaledChild() { + return Object.assign(new EventEmitter(), { + exitCode: null, + signalCode: 'SIGTERM' as NodeJS.Signals, + pid: undefined, + kill: vi.fn(), + }) as unknown as ChildProcess; +} + +afterEach(() => { + vi.unstubAllGlobals(); +}); + +test('waitForServer fails immediately when the server exited by signal', async () => { + const fetchMock = vi.fn().mockResolvedValue({ status: 200 }); + vi.stubGlobal('fetch', fetchMock); + + await expect(waitForServer('http://127.0.0.1:61812', signaledChild())) + .rejects.toThrow('Misskey server exited early with signal SIGTERM'); + expect(fetchMock).not.toHaveBeenCalled(); +}); + +test('stopServer returns immediately when the server already exited by signal', async () => { + const child = signaledChild(); + const stopPromise = stopServer(child); + + expect(child.listenerCount('exit')).toBe(0); + await stopPromise; +}); diff --git a/packages-private/diagnostics-frontend-bundle/test/fixtures/before-stats.json b/packages-private/diagnostics-frontend/test/bundle/fixtures/base-stats.json similarity index 99% rename from packages-private/diagnostics-frontend-bundle/test/fixtures/before-stats.json rename to packages-private/diagnostics-frontend/test/bundle/fixtures/base-stats.json index 0a54b93148..27f6b2d6b4 100644 --- a/packages-private/diagnostics-frontend-bundle/test/fixtures/before-stats.json +++ b/packages-private/diagnostics-frontend/test/bundle/fixtures/base-stats.json @@ -1 +1 @@ -{"nodeParts": {"p0": {"renderedLength": 1000, "gzipLength": 300, "brotliLength": 250}, "p1": {"renderedLength": 2000, "gzipLength": 600, "brotliLength": 500}, "p2": {"renderedLength": 3000, "gzipLength": 900, "brotliLength": 750}, "p3": {"renderedLength": 4000, "gzipLength": 1200, "brotliLength": 1000}, "p4": {"renderedLength": 5000, "gzipLength": 1500, "brotliLength": 1250}, "p5": {"renderedLength": 6000, "gzipLength": 1800, "brotliLength": 1500}}, "nodeMetas": {"m0": {"id": "/src/mod0.ts", "isEntry": true, "importedBy": [], "imported": [{"id": "m1", "dynamic": true}], "moduleParts": {"assets/boot-a1.js": "p0"}, "renderedLength": 0, "gzipLength": 0, "brotliLength": 0}, "m1": {"id": "/src/mod1.ts", "isEntry": false, "importedBy": ["m0"], "imported": [{"id": "m2", "dynamic": false}], "moduleParts": {"assets/vue-b2.js": "p1"}, "renderedLength": 0, "gzipLength": 0, "brotliLength": 0}, "m2": {"id": "/src/mod2.ts", "isEntry": false, "importedBy": ["m1"], "imported": [{"id": "m3", "dynamic": true}], "moduleParts": {"assets/boot-a1.js": "p2"}, "renderedLength": 0, "gzipLength": 0, "brotliLength": 0}, "m3": {"id": "/src/mod3.ts", "isEntry": false, "importedBy": ["m2"], "imported": [{"id": "m4", "dynamic": false}], "moduleParts": {"assets/vue-b2.js": "p3"}, "renderedLength": 0, "gzipLength": 0, "brotliLength": 0}, "m4": {"id": "/src/mod4.ts", "isEntry": false, "importedBy": ["m3"], "imported": [{"id": "m5", "dynamic": true}], "moduleParts": {"assets/boot-a1.js": "p4"}, "renderedLength": 0, "gzipLength": 0, "brotliLength": 0}, "m5": {"id": "/src/mod5.ts", "isEntry": false, "importedBy": ["m4"], "imported": [], "moduleParts": {"assets/vue-b2.js": "p5"}, "renderedLength": 0, "gzipLength": 0, "brotliLength": 0}}, "options": {}} \ No newline at end of file +{"nodeParts": {"p0": {"renderedLength": 1000, "gzipLength": 300, "brotliLength": 250}, "p1": {"renderedLength": 2000, "gzipLength": 600, "brotliLength": 500}, "p2": {"renderedLength": 3000, "gzipLength": 900, "brotliLength": 750}, "p3": {"renderedLength": 4000, "gzipLength": 1200, "brotliLength": 1000}, "p4": {"renderedLength": 5000, "gzipLength": 1500, "brotliLength": 1250}, "p5": {"renderedLength": 6000, "gzipLength": 1800, "brotliLength": 1500}}, "nodeMetas": {"m0": {"id": "/src/mod0.ts", "isEntry": true, "importedBy": [], "imported": [{"id": "m1", "dynamic": true}], "moduleParts": {"assets/boot-a1.js": "p0"}, "renderedLength": 0, "gzipLength": 0, "brotliLength": 0}, "m1": {"id": "/src/mod1.ts", "isEntry": false, "importedBy": ["m0"], "imported": [{"id": "m2", "dynamic": false}], "moduleParts": {"assets/vue-b2.js": "p1"}, "renderedLength": 0, "gzipLength": 0, "brotliLength": 0}, "m2": {"id": "/src/mod2.ts", "isEntry": false, "importedBy": ["m1"], "imported": [{"id": "m3", "dynamic": true}], "moduleParts": {"assets/boot-a1.js": "p2"}, "renderedLength": 0, "gzipLength": 0, "brotliLength": 0}, "m3": {"id": "/src/mod3.ts", "isEntry": false, "importedBy": ["m2"], "imported": [{"id": "m4", "dynamic": false}], "moduleParts": {"assets/vue-b2.js": "p3"}, "renderedLength": 0, "gzipLength": 0, "brotliLength": 0}, "m4": {"id": "/src/mod4.ts", "isEntry": false, "importedBy": ["m3"], "imported": [{"id": "m5", "dynamic": true}], "moduleParts": {"assets/boot-a1.js": "p4"}, "renderedLength": 0, "gzipLength": 0, "brotliLength": 0}, "m5": {"id": "/src/mod5.ts", "isEntry": false, "importedBy": ["m4"], "imported": [], "moduleParts": {"assets/vue-b2.js": "p5"}, "renderedLength": 0, "gzipLength": 0, "brotliLength": 0}}, "options": {}} diff --git a/packages-private/diagnostics-frontend-bundle/test/fixtures/after-stats.json b/packages-private/diagnostics-frontend/test/bundle/fixtures/head-stats.json similarity index 98% rename from packages-private/diagnostics-frontend-bundle/test/fixtures/after-stats.json rename to packages-private/diagnostics-frontend/test/bundle/fixtures/head-stats.json index b69e3f4cea..ecddcae812 100644 --- a/packages-private/diagnostics-frontend-bundle/test/fixtures/after-stats.json +++ b/packages-private/diagnostics-frontend/test/bundle/fixtures/head-stats.json @@ -1 +1 @@ -{"nodeParts": {"p0": {"renderedLength": 1100.0, "gzipLength": 300, "brotliLength": 250}, "p1": {"renderedLength": 2200.0, "gzipLength": 600, "brotliLength": 500}, "p2": {"renderedLength": 3300.0000000000005, "gzipLength": 900, "brotliLength": 750}, "p3": {"renderedLength": 4400.0, "gzipLength": 1200, "brotliLength": 1000}, "p4": {"renderedLength": 5500.0, "gzipLength": 1500, "brotliLength": 1250}, "p5": {"renderedLength": 6600.000000000001, "gzipLength": 1800, "brotliLength": 1500}, "p6": {"renderedLength": 7700.000000000001, "gzipLength": 2100, "brotliLength": 1750}}, "nodeMetas": {"m0": {"id": "/src/mod0.ts", "isEntry": true, "importedBy": [], "imported": [{"id": "m1", "dynamic": true}], "moduleParts": {"assets/boot-a1.js": "p0"}, "renderedLength": 0, "gzipLength": 0, "brotliLength": 0}, "m1": {"id": "/src/mod1.ts", "isEntry": false, "importedBy": ["m0"], "imported": [{"id": "m2", "dynamic": false}], "moduleParts": {"assets/vue-b2.js": "p1"}, "renderedLength": 0, "gzipLength": 0, "brotliLength": 0}, "m2": {"id": "/src/mod2.ts", "isEntry": false, "importedBy": ["m1"], "imported": [{"id": "m3", "dynamic": true}], "moduleParts": {"assets/boot-a1.js": "p2"}, "renderedLength": 0, "gzipLength": 0, "brotliLength": 0}, "m3": {"id": "/src/mod3.ts", "isEntry": false, "importedBy": ["m2"], "imported": [{"id": "m4", "dynamic": false}], "moduleParts": {"assets/vue-b2.js": "p3"}, "renderedLength": 0, "gzipLength": 0, "brotliLength": 0}, "m4": {"id": "/src/mod4.ts", "isEntry": false, "importedBy": ["m3"], "imported": [{"id": "m5", "dynamic": true}], "moduleParts": {"assets/boot-a1.js": "p4"}, "renderedLength": 0, "gzipLength": 0, "brotliLength": 0}, "m5": {"id": "/src/mod5.ts", "isEntry": false, "importedBy": ["m4"], "imported": [{"id": "m6", "dynamic": false}], "moduleParts": {"assets/vue-b2.js": "p5"}, "renderedLength": 0, "gzipLength": 0, "brotliLength": 0}, "m6": {"id": "/src/mod6.ts", "isEntry": false, "importedBy": ["m5"], "imported": [], "moduleParts": {"assets/boot-a1.js": "p6"}, "renderedLength": 0, "gzipLength": 0, "brotliLength": 0}}, "options": {}} \ No newline at end of file +{"nodeParts": {"p0": {"renderedLength": 1100.0, "gzipLength": 300, "brotliLength": 250}, "p1": {"renderedLength": 2200.0, "gzipLength": 600, "brotliLength": 500}, "p2": {"renderedLength": 3300.0000000000005, "gzipLength": 900, "brotliLength": 750}, "p3": {"renderedLength": 4400.0, "gzipLength": 1200, "brotliLength": 1000}, "p4": {"renderedLength": 5500.0, "gzipLength": 1500, "brotliLength": 1250}, "p5": {"renderedLength": 6600.000000000001, "gzipLength": 1800, "brotliLength": 1500}, "p6": {"renderedLength": 7700.000000000001, "gzipLength": 2100, "brotliLength": 1750}}, "nodeMetas": {"m0": {"id": "/src/mod0.ts", "isEntry": true, "importedBy": [], "imported": [{"id": "m1", "dynamic": true}], "moduleParts": {"assets/boot-a1.js": "p0"}, "renderedLength": 0, "gzipLength": 0, "brotliLength": 0}, "m1": {"id": "/src/mod1.ts", "isEntry": false, "importedBy": ["m0"], "imported": [{"id": "m2", "dynamic": false}], "moduleParts": {"assets/vue-b2.js": "p1"}, "renderedLength": 0, "gzipLength": 0, "brotliLength": 0}, "m2": {"id": "/src/mod2.ts", "isEntry": false, "importedBy": ["m1"], "imported": [{"id": "m3", "dynamic": true}], "moduleParts": {"assets/boot-a1.js": "p2"}, "renderedLength": 0, "gzipLength": 0, "brotliLength": 0}, "m3": {"id": "/src/mod3.ts", "isEntry": false, "importedBy": ["m2"], "imported": [{"id": "m4", "dynamic": false}], "moduleParts": {"assets/vue-b2.js": "p3"}, "renderedLength": 0, "gzipLength": 0, "brotliLength": 0}, "m4": {"id": "/src/mod4.ts", "isEntry": false, "importedBy": ["m3"], "imported": [{"id": "m5", "dynamic": true}], "moduleParts": {"assets/boot-a1.js": "p4"}, "renderedLength": 0, "gzipLength": 0, "brotliLength": 0}, "m5": {"id": "/src/mod5.ts", "isEntry": false, "importedBy": ["m4"], "imported": [{"id": "m6", "dynamic": false}], "moduleParts": {"assets/vue-b2.js": "p5"}, "renderedLength": 0, "gzipLength": 0, "brotliLength": 0}, "m6": {"id": "/src/mod6.ts", "isEntry": false, "importedBy": ["m5"], "imported": [], "moduleParts": {"assets/boot-a1.js": "p6"}, "renderedLength": 0, "gzipLength": 0, "brotliLength": 0}}, "options": {}} diff --git a/packages-private/diagnostics-frontend/test/bundle/fs-utils.test.ts b/packages-private/diagnostics-frontend/test/bundle/fs-utils.test.ts new file mode 100644 index 0000000000..12ec8d7bb1 --- /dev/null +++ b/packages-private/diagnostics-frontend/test/bundle/fs-utils.test.ts @@ -0,0 +1,29 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { promises as fs } from 'node:fs'; +import { afterEach, expect, test, vi } from 'vitest'; +import { fileExists } from '../../src/bundle/fs-utils'; + +function fsError(code: string) { + return Object.assign(new Error(`fs error: ${code}`), { code }) as NodeJS.ErrnoException; +} + +afterEach(() => { + vi.restoreAllMocks(); +}); + +test('fileExists returns false for ENOENT', async () => { + vi.spyOn(fs, 'access').mockRejectedValueOnce(fsError('ENOENT')); + + await expect(fileExists('missing')).resolves.toBe(false); +}); + +test('fileExists rethrows errors other than ENOENT', async () => { + const error = fsError('EACCES'); + vi.spyOn(fs, 'access').mockRejectedValueOnce(error); + + await expect(fileExists('restricted')).rejects.toBe(error); +}); diff --git a/packages-private/diagnostics-frontend/test/bundle/manifest.test.ts b/packages-private/diagnostics-frontend/test/bundle/manifest.test.ts new file mode 100644 index 0000000000..ed91120f5f --- /dev/null +++ b/packages-private/diagnostics-frontend/test/bundle/manifest.test.ts @@ -0,0 +1,24 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { mkdtemp, rm } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { afterAll, beforeAll, expect, test } from 'vitest'; +import { collectBundleReport } from '../../src/bundle/manifest'; + +let workDir: string; + +beforeAll(async () => { + workDir = await mkdtemp(join(tmpdir(), 'diagnostics-frontend-test-')); +}); + +afterAll(async () => { + await rm(workDir, { recursive: true, force: true }); +}); + +test('fails loudly when the built output is missing', async () => { + await expect(collectBundleReport(join(workDir, 'nonexistent'))).rejects.toThrow(); +}); diff --git a/packages-private/diagnostics-frontend-bundle/test/render-md.test.ts b/packages-private/diagnostics-frontend/test/report.test.ts similarity index 53% rename from packages-private/diagnostics-frontend-bundle/test/render-md.test.ts rename to packages-private/diagnostics-frontend/test/report.test.ts index 17adcef883..c45961ccb9 100644 --- a/packages-private/diagnostics-frontend-bundle/test/render-md.test.ts +++ b/packages-private/diagnostics-frontend/test/report.test.ts @@ -7,16 +7,18 @@ import { mkdtemp, mkdir, readFile, rm, writeFile } from 'node:fs/promises'; import { tmpdir } from 'node:os'; import { dirname, join } from 'node:path'; import { afterAll, beforeAll, expect, test } from 'vitest'; -import { collectReport } from '../src/manifest'; -import { renderBundleReportMarkdown } from '../src/report'; -import type { VisualizerReport } from '../src/visualizer'; +import { collectBundleReport } from '../src/bundle/manifest'; +import { renderFrontendDiagnosticsMarkdown } from '../src/report'; +import type { BrowserMetricsReport } from '../src/browser/types'; +import type { VisualizerReport } from '../src/bundle/visualizer'; -const fixturesDir = join(import.meta.dirname, 'fixtures'); +const bundleFixturesDir = join(import.meta.dirname, 'bundle/fixtures'); +const browserFixturesDir = join(import.meta.dirname, 'browser/fixtures'); /** * ビルド成果物のfixture。 * - * `collectReport` はファイルの中身を見ずサイズしか使わないので、実体は指定バイト数の + * `collectBundleReport` はファイルの中身を見ずサイズしか使わないので、実体は指定バイト数の * 詰め物でよい。ディレクトリ名が `built` になるためリポジトリにはコミットできず * (ルートの .gitignore がビルド成果物として除外する)、テスト実行時に組み立てている。 */ @@ -31,7 +33,7 @@ const manifest = { }; const fileSizes = { - before: { + base: { 'assets/boot-a1.js': 20_000, 'assets/vue-b2.js': 90_000, 'assets/foo-d4.js': 5_000, @@ -39,7 +41,7 @@ const fileSizes = { 'ja-JP/i18n-c3.js': 4_000, 'ja-JP/orphan.js': 1_200, }, - after: { + head: { // 差が小さすぎる (閾値5バイト以下) ので「(other)」に集約される 'assets/boot-a1.js': 20_003, // 明確に増えるので diff表に行として出る @@ -50,15 +52,15 @@ const fileSizes = { // manifestに載らない出力なので「(other generated chunks)」に集約される 'ja-JP/orphan.js': 1_500, }, -} as const satisfies Record<'before' | 'after', Record>; +} as const satisfies Record<'base' | 'head', Record>; -let repoDirs: { before: string; after: string }; +let repoDirs: { base: string; head: string }; let workDir: string; beforeAll(async () => { - workDir = await mkdtemp(join(tmpdir(), 'diagnostics-frontend-bundle-')); + workDir = await mkdtemp(join(tmpdir(), 'diagnostics-frontend-report-test-')); - for (const label of ['before', 'after'] as const) { + for (const label of ['base', 'head'] as const) { const outDir = join(workDir, label, 'built/_frontend_vite_'); await mkdir(outDir, { recursive: true }); await writeFile(join(outDir, 'manifest.json'), JSON.stringify(manifest)); @@ -71,8 +73,8 @@ beforeAll(async () => { } repoDirs = { - before: join(workDir, 'before'), - after: join(workDir, 'after'), + base: join(workDir, 'base'), + head: join(workDir, 'head'), }; }); @@ -80,26 +82,45 @@ afterAll(async () => { await rm(workDir, { recursive: true, force: true }); }); -async function loadStats(name: string) { - return JSON.parse(await readFile(join(fixturesDir, `${name}-stats.json`), 'utf8')) as VisualizerReport; +async function loadBundleStats(name: 'base' | 'head') { + return JSON.parse(await readFile(join(bundleFixturesDir, `${name}-stats.json`), 'utf8')) as VisualizerReport; +} + +async function loadBrowserReport(name: 'base' | 'head') { + return JSON.parse(await readFile(join(browserFixturesDir, `${name}.json`), 'utf8')) as BrowserMetricsReport; +} + +async function renderReport(detailedHtmlUrl: string | null) { + return renderFrontendDiagnosticsMarkdown({ + bundle: { + base: await collectBundleReport(repoDirs.base), + head: await collectBundleReport(repoDirs.head), + baseStats: await loadBundleStats('base'), + headStats: await loadBundleStats('head'), + visualizerArtifactUrl: 'https://example.invalid/treemap', + }, + browser: { + base: await loadBrowserReport('base'), + head: await loadBrowserReport('head'), + baseHeapSnapshotUrl: 'https://example.invalid/base', + headHeapSnapshotUrl: 'https://example.invalid/head', + detailedHtmlUrl, + }, + }); } /** * 出力をゴールデンファイルで固定する。 * 意図的に変更したときは `vitest -u` で更新し、__snapshots__ の差分もレビューすること。 */ -test('renders the frontend bundle report', async () => { - const markdown = renderBundleReportMarkdown( - await collectReport(repoDirs.before), - await collectReport(repoDirs.after), - await loadStats('before'), - await loadStats('after'), - { visualizerArtifactUrl: 'https://example.invalid/treemap' }, - ); +test('renders one frontend diagnostics markdown report from bundle and browser data', async () => { + const markdown = await renderReport('https://example.invalid/html'); - await expect(markdown).toMatchFileSnapshot('./__snapshots__/render-md.md'); + await expect(markdown).toMatchFileSnapshot('./__snapshots__/report.md'); }); -test('fails loudly when the built output is missing', async () => { - await expect(collectReport(join(workDir, 'nonexistent'))).rejects.toThrow(); +test('omits the browser details link when no detailed html artifact was uploaded', async () => { + const markdown = await renderReport(null); + + expect(markdown).not.toContain('View details'); }); diff --git a/packages-private/diagnostics-frontend-browser/tsconfig.json b/packages-private/diagnostics-frontend/tsconfig.json similarity index 100% rename from packages-private/diagnostics-frontend-browser/tsconfig.json rename to packages-private/diagnostics-frontend/tsconfig.json diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 53a344d0e5..02293b4690 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -131,7 +131,7 @@ importers: specifier: 4.1.10 version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.1.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(happy-dom@20.10.6(bufferutil@4.1.0)(utf-8-validate@6.0.6))(msw@2.15.0(@types/node@26.1.1)(typescript@5.9.3))(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.1)) - packages-private/diagnostics-frontend-browser: + packages-private/diagnostics-frontend: dependencies: diagnostics-shared: specifier: workspace:* @@ -156,28 +156,6 @@ importers: specifier: 4.1.10 version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.1.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(happy-dom@20.10.6(bufferutil@4.1.0)(utf-8-validate@6.0.6))(msw@2.15.0(@types/node@26.1.1)(typescript@5.9.3))(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.1)) - packages-private/diagnostics-frontend-bundle: - dependencies: - diagnostics-shared: - specifier: workspace:* - version: link:../diagnostics-shared - devDependencies: - '@types/node': - specifier: 26.1.1 - version: 26.1.1 - tsx: - specifier: 4.23.1 - version: 4.23.1 - typescript: - specifier: 5.9.3 - version: 5.9.3 - vite: - specifier: 8.1.4 - version: 8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.1) - vitest: - specifier: 4.1.10 - version: 4.1.10(@opentelemetry/api@1.9.1)(@types/node@26.1.1)(@vitest/browser-playwright@4.1.10)(@vitest/coverage-v8@4.1.10)(happy-dom@20.10.6(bufferutil@4.1.0)(utf-8-validate@6.0.6))(msw@2.15.0(@types/node@26.1.1)(typescript@5.9.3))(vite@8.1.4(@types/node@26.1.1)(esbuild@0.28.1)(sass-embedded@1.100.0)(sass@1.100.0)(terser@5.48.0)(tsx@4.23.1)) - packages-private/diagnostics-shared: devDependencies: '@types/node':