feat: setup analytics-mailer with gitea cron pipeline for e-tib and klz
Some checks failed
Monorepo Pipeline / ⚡ Prioritize Release (push) Successful in 3s
Monorepo Pipeline / 🧹 Lint (push) Failing after 1m42s
Monorepo Pipeline / 🧪 Test (push) Successful in 56s
Monorepo Pipeline / 🏗️ Build (push) Successful in 2m35s
Monorepo Pipeline / 🚀 Release (push) Has been skipped
Monorepo Pipeline / 🐳 Build Gatekeeper (Product) (push) Has been skipped
Monorepo Pipeline / 🐳 Build Build-Base (push) Has been skipped
Monorepo Pipeline / 🐳 Build Production Runtime (push) Has been skipped
Monthly Analytics Mailer / 📊 Send Monthly Reports (push) Failing after 59s
🏥 Server Maintenance / 🧹 Prune & Clean (push) Failing after 8s

This commit is contained in:
2026-06-21 20:52:31 +02:00
parent 985e532c61
commit 7a9dd06fc6
8 changed files with 509 additions and 7 deletions

View File

@@ -0,0 +1,44 @@
name: Monthly Analytics Mailer
on:
schedule:
- cron: '0 8 1 * *' # Run at 08:00 on the 1st of every month
workflow_dispatch: # Allow manual trigger
jobs:
send-reports:
name: 📊 Send Monthly Reports
runs-on: docker
container:
image: catthehacker/ubuntu:act-latest
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
UMAMI_BASE_URL: ${{ secrets.UMAMI_BASE_URL }}
UMAMI_USERNAME: ${{ secrets.UMAMI_USERNAME }}
UMAMI_PASSWORD: ${{ secrets.UMAMI_PASSWORD }}
MAILGUN_API_KEY: ${{ secrets.MAILGUN_API_KEY }}
MAILGUN_DOMAIN: ${{ secrets.MAILGUN_DOMAIN }}
MAILGUN_SENDER: ${{ secrets.MAILGUN_SENDER }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node_version: 20
- name: Enable pnpm
run: corepack enable && corepack prepare pnpm@10.2.0 --activate
- name: Install dependencies
run: pnpm install --frozen-lockfile --prefer-offline --ignore-scripts --no-color
- name: Build mail templates
run: pnpm --filter @mintel/mail build
- name: Build analytics mailer
run: pnpm --filter @mintel/analytics-mailer build
- name: Run Analytics Mailer
run: pnpm --filter @mintel/analytics-mailer run start

View File

@@ -0,0 +1,14 @@
[
{
"websiteId": "d773ea10-a3b3-4ccf-9024-987e14c4d669",
"domain": "e-tib.com",
"clientEmail": "marc@mintel.me",
"clientName": "E-TIB GmbH"
},
{
"websiteId": "59a7db94-0100-4c7e-98ef-99f45b17f9c3",
"domain": "klz-cables.com",
"clientEmail": "marc@mintel.me",
"clientName": "KLZ"
}
]

View File

@@ -0,0 +1,26 @@
{
"name": "@mintel/analytics-mailer",
"version": "1.0.0",
"private": true,
"type": "module",
"scripts": {
"build": "tsup src/index.ts --format esm --clean",
"start": "node dist/index.js",
"dev": "tsx src/index.ts"
},
"dependencies": {
"@mintel/mail": "workspace:*",
"axios": "^1.7.9",
"dotenv": "^16.4.7",
"form-data": "^4.0.1",
"mailgun.js": "^10.3.0"
},
"devDependencies": {
"@mintel/eslint-config": "workspace:*",
"@mintel/tsconfig": "workspace:*",
"@types/node": "^22.10.2",
"tsup": "^8.3.5",
"tsx": "^4.19.2",
"typescript": "^5.7.2"
}
}

View File

@@ -0,0 +1,143 @@
import fs from "fs/promises";
import path from "path";
import axios from "axios";
import { render } from "@mintel/mail";
import { MonthlyAnalyticsTemplate } from "@mintel/mail";
import Mailgun from "mailgun.js";
import formData from "form-data";
import * as dotenv from "dotenv";
dotenv.config();
const UMAMI_BASE_URL = process.env.UMAMI_BASE_URL || "https://umami.infra.mintel.me";
const UMAMI_USERNAME = process.env.UMAMI_USERNAME;
const UMAMI_PASSWORD = process.env.UMAMI_PASSWORD;
const UMAMI_API_KEY = process.env.UMAMI_API_KEY;
const MAILGUN_API_KEY = process.env.MAILGUN_API_KEY;
const MAILGUN_DOMAIN = process.env.MAILGUN_DOMAIN;
const MAILGUN_SENDER = process.env.MAILGUN_SENDER || "reports@mintel.me";
const MAILGUN_URL = process.env.MAILGUN_URL || "https://api.eu.mailgun.net";
interface ConfigClient {
websiteId: string;
domain: string;
clientEmail: string;
clientName: string;
}
async function getUmamiToken(): Promise<string> {
if (UMAMI_API_KEY) {
return UMAMI_API_KEY;
}
if (!UMAMI_USERNAME || !UMAMI_PASSWORD) {
throw new Error("Missing UMAMI_USERNAME and UMAMI_PASSWORD or UMAMI_API_KEY");
}
const res = await axios.post(`${UMAMI_BASE_URL}/api/auth/login`, {
username: UMAMI_USERNAME,
password: UMAMI_PASSWORD,
});
return res.data.token;
}
async function fetchUmamiData(api: any, websiteId: string, startAt: number, endAt: number) {
const [statsRes, pagesRes, refRes] = await Promise.all([
api.get(`/websites/${websiteId}/stats`, { params: { startAt, endAt } }),
api.get(`/websites/${websiteId}/metrics`, { params: { type: "url", limit: 5, startAt, endAt } }),
api.get(`/websites/${websiteId}/metrics`, { params: { type: "referrer", limit: 3, startAt, endAt } }),
]);
const stats = statsRes.data;
const topPages = (pagesRes.data || []).map((p: any) => ({ url: p.x, views: p.y }));
const topReferrers = (refRes.data || []).map((r: any) => ({ url: r.x, visitors: r.y }));
return {
totalVisitors: stats.visitors?.value || 0,
totalPageviews: stats.pageviews?.value || 0,
topPages,
topReferrers,
};
}
async function main() {
const isDryRun = process.argv.includes("--dry-run");
// Load config
const configPath = path.resolve(process.cwd(), "config.json");
const configRaw = await fs.readFile(configPath, "utf-8");
const clients: ConfigClient[] = JSON.parse(configRaw);
// Setup Umami API
const token = await getUmamiToken();
const headers = UMAMI_API_KEY ? { "x-umami-api-key": token } : { Authorization: `Bearer ${token}` };
const api = axios.create({ baseURL: `${UMAMI_BASE_URL}/api`, headers });
// Setup Mailgun
const mailgun = new Mailgun(formData);
const mg = mailgun.client({ username: "api", key: MAILGUN_API_KEY!, url: MAILGUN_URL });
// Time range calculation (previous month)
const now = new Date();
// Ensure we get the correct previous month relative to current local time, but use UTC boundary or local? Umami uses ms timestamps.
// Standard way: First day of previous month, last day of previous month.
const startOfPrevMonth = new Date(now.getFullYear(), now.getMonth() - 1, 1);
const endOfPrevMonth = new Date(now.getFullYear(), now.getMonth(), 0, 23, 59, 59, 999);
const startAt = startOfPrevMonth.getTime();
const endAt = endOfPrevMonth.getTime();
const monthName = startOfPrevMonth.toLocaleString("de-DE", { month: "long" });
const year = startOfPrevMonth.getFullYear().toString();
console.log(`Generating reports for ${monthName} ${year}...`);
for (const client of clients) {
if (client.websiteId === "replace-with-umami-website-id") continue;
console.log(`Processing ${client.domain} (${client.clientName})...`);
try {
const data = await fetchUmamiData(api, client.websiteId, startAt, endAt);
const html = await render(MonthlyAnalyticsTemplate({
clientName: client.clientName,
domain: client.domain,
month: monthName,
year,
totalVisitors: data.totalVisitors,
totalPageviews: data.totalPageviews,
topPages: data.topPages,
topReferrers: data.topReferrers,
}));
const subject = `Ihr Website-Report für ${monthName} ${year} - ${client.domain}`;
if (isDryRun) {
console.log(`[DRY-RUN] Would send email to ${client.clientEmail} for ${client.domain}`);
const debugPath = path.resolve(process.cwd(), `dry-run-${client.domain}.html`);
await fs.writeFile(debugPath, html);
console.log(`[DRY-RUN] Saved HTML preview to ${debugPath}`);
} else {
if (!MAILGUN_API_KEY) {
throw new Error("MAILGUN_API_KEY is missing for live run.");
}
await mg.messages.create(MAILGUN_DOMAIN!, {
from: MAILGUN_SENDER,
to: [client.clientEmail],
subject,
html,
});
console.log(`Successfully sent to ${client.clientEmail}`);
}
} catch (e: any) {
console.error(`Failed to process client ${client.domain}:`, e.response?.data || e.message);
}
}
console.log("Done.");
}
main().catch((err) => {
console.error("Fatal error:", err);
process.exit(1);
});

View File

@@ -0,0 +1,10 @@
{
"extends": "@mintel/tsconfig/base.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src",
"module": "NodeNext",
"moduleResolution": "NodeNext"
},
"include": ["src/**/*"]
}

View File

@@ -25,3 +25,4 @@ export * from "./templates/ConfirmationMessage";
export * from "./templates/FollowUpTemplate";
export * from "./templates/ProjectEstimateTemplate";
export * from "./templates/SiteAuditTemplate";
export * from "./templates/MonthlyAnalyticsTemplate";

View File

@@ -0,0 +1,205 @@
import * as React from "react";
import { Heading, Section, Text, Row, Column } from "@react-email/components";
import { MintelLayout } from "../layouts/MintelLayout";
export interface MonthlyAnalyticsTemplateProps {
clientName: string;
domain: string;
month: string;
year: string;
totalVisitors: number;
totalPageviews: number;
topPages: { url: string; views: number }[];
topReferrers: { url: string; visitors: number }[];
}
export const MonthlyAnalyticsTemplate = ({
clientName,
domain,
month,
year,
totalVisitors,
totalPageviews,
topPages,
topReferrers,
}: MonthlyAnalyticsTemplateProps) => {
const preview = `Ihr Website-Report für ${month} ${year}: ${domain}`;
return (
<MintelLayout preview={preview}>
<Heading style={h1}>Monatsreport: {month} {year}</Heading>
<Text style={intro}>
Hallo {clientName},<br /><br />
hier ist der monatliche Performance-Report für Ihre Website ({domain}).
Die Daten zeigen, wie viele Menschen Ihre Seite besucht haben und welche Inhalte am beliebtesten waren.
</Text>
<Section style={metricsContainer}>
<Row>
<Column style={metricColumn}>
<Text style={metricLabel}>BESUCHER</Text>
<Text style={metricValue}>{totalVisitors.toLocaleString('de-DE')}</Text>
</Column>
<Column style={metricColumn}>
<Text style={metricLabel}>SEITENAUFRUFE</Text>
<Text style={metricValue}>{totalPageviews.toLocaleString('de-DE')}</Text>
</Column>
</Row>
</Section>
<Section style={auditContainer}>
<Heading as="h2" style={h2}>Top Seiten</Heading>
{topPages.map((page, i) => (
<Row key={i} style={highlightRow}>
<Column style={bulletCol}>
<div style={bullet} />
</Column>
<Column>
<Text style={highlightText}>{page.url}</Text>
</Column>
<Column align="right">
<Text style={highlightNumber}>{page.views.toLocaleString('de-DE')} Aufrufe</Text>
</Column>
</Row>
))}
</Section>
<Section style={auditContainer}>
<Heading as="h2" style={h2}>Top Besucherquellen</Heading>
{topReferrers.length > 0 ? topReferrers.map((ref, i) => (
<Row key={i} style={highlightRow}>
<Column style={bulletCol}>
<div style={bullet} />
</Column>
<Column>
<Text style={highlightText}>{ref.url || "Direktaufruf"}</Text>
</Column>
<Column align="right">
<Text style={highlightNumber}>{ref.visitors.toLocaleString('de-DE')} Besucher</Text>
</Column>
</Row>
)) : (
<Text style={highlightText}>Keine externen Quellen verzeichnet.</Text>
)}
</Section>
<Text style={bodyText}>
Haben Sie Fragen zu diesen Zahlen oder möchten Sie die Reichweite Ihrer Seite weiter ausbauen?
Antworten Sie einfach auf diese E-Mail.
</Text>
<Text style={footerText}>
Beste Grüße,<br />
<strong>Marc Mintel</strong><br />
Digitaler Architekt
</Text>
</MintelLayout>
);
};
export default MonthlyAnalyticsTemplate;
const h1 = {
fontSize: "28px",
fontWeight: "900",
margin: "0 0 24px",
color: "#ffffff",
letterSpacing: "-0.04em",
};
const h2 = {
fontSize: "14px",
fontWeight: "900",
textTransform: "uppercase" as const,
color: "#444444",
margin: "0 0 16px",
letterSpacing: "0.1em",
};
const intro = {
fontSize: "16px",
lineHeight: "24px",
color: "#cccccc",
margin: "0 0 32px",
};
const metricsContainer = {
backgroundColor: "#151515",
padding: "32px",
borderRadius: "8px",
marginBottom: "32px",
border: "1px solid #222222",
textAlign: "center" as const,
};
const metricColumn = {
width: "50%",
};
const metricLabel = {
fontSize: "12px",
fontWeight: "bold",
color: "#888888",
letterSpacing: "0.1em",
margin: "0 0 8px",
};
const metricValue = {
fontSize: "36px",
fontWeight: "900",
color: "#4CAF50",
margin: "0",
letterSpacing: "-0.02em",
};
const auditContainer = {
backgroundColor: "#151515",
padding: "32px",
borderRadius: "8px",
marginBottom: "32px",
border: "1px solid #222222",
};
const highlightRow = {
marginBottom: "12px",
};
const bulletCol = {
width: "24px",
verticalAlign: "top" as const,
};
const bullet = {
width: "6px",
height: "6px",
backgroundColor: "#4CAF50",
marginTop: "8px",
};
const highlightText = {
fontSize: "15px",
color: "#ffffff",
margin: "0",
lineHeight: "22px",
wordBreak: "break-all" as const,
};
const highlightNumber = {
fontSize: "15px",
color: "#888888",
margin: "0",
whiteSpace: "nowrap" as const,
};
const bodyText = {
fontSize: "16px",
lineHeight: "24px",
color: "#888888",
margin: "0 0 32px",
};
const footerText = {
fontSize: "14px",
color: "#666666",
lineHeight: "20px",
};

73
pnpm-lock.yaml generated
View File

@@ -104,6 +104,43 @@ importers:
specifier: ^4.0.18
version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@20.19.33)(@vitest/ui@4.0.18)(happy-dom@20.5.3)(jiti@2.6.1)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
apps/analytics-mailer:
dependencies:
'@mintel/mail':
specifier: workspace:*
version: link:../../packages/mail
axios:
specifier: ^1.7.9
version: 1.13.5
dotenv:
specifier: ^16.4.7
version: 16.6.1
form-data:
specifier: ^4.0.1
version: 4.0.5
mailgun.js:
specifier: ^10.3.0
version: 10.4.0
devDependencies:
'@mintel/eslint-config':
specifier: workspace:*
version: link:../../packages/eslint-config
'@mintel/tsconfig':
specifier: workspace:*
version: link:../../packages/tsconfig
'@types/node':
specifier: ^22.10.2
version: 22.19.10
tsup:
specifier: ^8.3.5
version: 8.5.1(@swc/core@1.15.11(@swc/helpers@0.5.18))(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2)
tsx:
specifier: ^4.19.2
version: 4.21.0
typescript:
specifier: ^5.7.2
version: 5.9.3
apps/sample-website:
dependencies:
'@mintel/next-observability':
@@ -235,7 +272,7 @@ importers:
version: 5.9.3
vitest:
specifier: ^3.0.5
version: 3.2.4(@types/debug@4.1.12)(@types/node@20.19.33)(@vitest/ui@4.0.18(vitest@4.0.18))(happy-dom@20.5.3)(jiti@2.6.1)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
version: 3.2.4(@types/debug@4.1.12)(@types/node@20.19.33)(@vitest/ui@4.0.18)(happy-dom@20.5.3)(jiti@2.6.1)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
packages/content-engine:
dependencies:
@@ -537,7 +574,7 @@ importers:
version: 5.9.3
vitest:
specifier: ^3.0.4
version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.10)(@vitest/ui@4.0.18(vitest@4.0.18))(happy-dom@20.5.3)(jiti@2.6.1)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
version: 3.2.4(@types/debug@4.1.12)(@types/node@22.19.10)(@vitest/ui@4.0.18)(happy-dom@20.5.3)(jiti@2.6.1)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
packages/meme-generator:
dependencies:
@@ -752,7 +789,7 @@ importers:
version: 5.9.3
vitest:
specifier: ^2.0.0
version: 2.1.9(@types/node@22.19.10)(@vitest/ui@4.0.18(vitest@4.0.18))(happy-dom@20.5.3)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)
version: 2.1.9(@types/node@22.19.10)(@vitest/ui@4.0.18)(happy-dom@20.5.3)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)
packages/outline-mcp:
dependencies:
@@ -928,7 +965,7 @@ importers:
version: 5.9.3
vitest:
specifier: ^3.0.5
version: 3.2.4(@types/debug@4.1.12)(@types/node@20.19.33)(@vitest/ui@4.0.18(vitest@4.0.18))(happy-dom@20.5.3)(jiti@2.6.1)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
version: 3.2.4(@types/debug@4.1.12)(@types/node@20.19.33)(@vitest/ui@4.0.18)(happy-dom@20.5.3)(jiti@2.6.1)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2)
packages/serpbear-mcp:
dependencies:
@@ -4234,6 +4271,9 @@ packages:
bare-url@2.3.2:
resolution: {integrity: sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw==}
base-64@1.0.0:
resolution: {integrity: sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==}
base64-arraybuffer@1.0.2:
resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==}
engines: {node: '>= 0.6.0'}
@@ -6406,6 +6446,10 @@ packages:
resolution: {integrity: sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==}
engines: {node: '>=12'}
mailgun.js@10.4.0:
resolution: {integrity: sha512-YrdaZEAJwwjXGBTfZTNQ1LM7tmkdUaz2NpZEu7+zULcG4Wrlhd7cWSNZW0bxT3bP48k5N0mZWz8C2f9gc2+Geg==}
engines: {node: '>=18.0.0'}
map-stream@0.1.0:
resolution: {integrity: sha512-CkYQrPYZfWnu/DAmVCpTSX/xHpKZ80eKh2lAkyA6AJTef6bW+6JpbQZN5rofum7da+SyN1bi5ctTm+lTfcCW3g==}
@@ -8285,6 +8329,9 @@ packages:
uri-js@4.4.1:
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
url-join@4.0.1:
resolution: {integrity: sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==}
use-context-selector@2.0.0:
resolution: {integrity: sha512-owfuSmUNd3eNp3J9CdDl0kMgfidV+MkDvHPpvthN5ThqM+ibMccNE0k+Iq7TWC6JPFvGZqanqiGCuQx6DyV24g==}
peerDependencies:
@@ -12297,6 +12344,8 @@ snapshots:
dependencies:
bare-path: 3.0.0
base-64@1.0.0: {}
base64-arraybuffer@1.0.2: {}
base64-js@0.0.8: {}
@@ -14780,6 +14829,14 @@ snapshots:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
mailgun.js@10.4.0:
dependencies:
axios: 1.13.5
base-64: 1.0.0
url-join: 4.0.1
transitivePeerDependencies:
- debug
map-stream@0.1.0: {}
marked@14.0.0: {}
@@ -16983,6 +17040,8 @@ snapshots:
dependencies:
punycode: 2.3.1
url-join@4.0.1: {}
use-context-selector@2.0.0(react@19.2.4)(scheduler@0.25.0):
dependencies:
react: 19.2.4
@@ -17206,7 +17265,7 @@ snapshots:
- supports-color
- terser
vitest@2.1.9(@types/node@22.19.10)(@vitest/ui@4.0.18(vitest@4.0.18))(happy-dom@20.5.3)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0):
vitest@2.1.9(@types/node@22.19.10)(@vitest/ui@4.0.18)(happy-dom@20.5.3)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0):
dependencies:
'@vitest/expect': 2.1.9
'@vitest/mocker': 2.1.9(vite@5.4.21(@types/node@22.19.10)(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0))
@@ -17244,7 +17303,7 @@ snapshots:
- supports-color
- terser
vitest@3.2.4(@types/debug@4.1.12)(@types/node@20.19.33)(@vitest/ui@4.0.18(vitest@4.0.18))(happy-dom@20.5.3)(jiti@2.6.1)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2):
vitest@3.2.4(@types/debug@4.1.12)(@types/node@20.19.33)(@vitest/ui@4.0.18)(happy-dom@20.5.3)(jiti@2.6.1)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2):
dependencies:
'@types/chai': 5.2.3
'@vitest/expect': 3.2.4
@@ -17289,7 +17348,7 @@ snapshots:
- tsx
- yaml
vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.19.10)(@vitest/ui@4.0.18(vitest@4.0.18))(happy-dom@20.5.3)(jiti@2.6.1)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2):
vitest@3.2.4(@types/debug@4.1.12)(@types/node@22.19.10)(@vitest/ui@4.0.18)(happy-dom@20.5.3)(jiti@2.6.1)(jsdom@27.4.0(canvas@3.2.1))(lightningcss@1.30.2)(sass@1.97.3)(terser@5.46.0)(tsx@4.21.0)(yaml@2.8.2):
dependencies:
'@types/chai': 5.2.3
'@vitest/expect': 3.2.4