diff --git a/Dockerfile b/Dockerfile index 4ceb43b..028b2cd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,7 +10,9 @@ RUN npm install -g corepack && corepack enable COPY . . RUN pnpm install --frozen-lockfile ARG BASE_PATH=/ -RUN BASE_PATH="$BASE_PATH" pnpm --filter web build +# .git isn't in the build context, so pass the version in: --build-arg APP_VERSION=v1.2.0 +ARG APP_VERSION=dev +RUN BASE_PATH="$BASE_PATH" APP_VERSION="$APP_VERSION" pnpm --filter web build FROM nginxinc/nginx-unprivileged:1.29-alpine AS serve ARG BASE_PATH=/ diff --git a/apps/web/package.json b/apps/web/package.json index ccef632..d252a0f 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -31,6 +31,7 @@ "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^14.6.1", + "@types/node": "^26.6.2", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", "@types/web-bluetooth": "^0.0.21", diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 560e7c2..70c420b 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -27,6 +27,7 @@ import { ChartColorPicker } from './components/ChartColorPicker'; import { ShortcutsHelp } from './components/ShortcutsHelp'; import { UnsupportedBrowser } from './components/UnsupportedBrowser'; import { ReportToast } from './components/ReportToast'; +import { AppFooter } from './components/AppFooter'; import { exportCsv, exportPng } from './lib/exporters'; // Code-split the uPlot-heavy chart + the whole Recordings view so the initial bundle stays lean. @@ -359,19 +360,7 @@ export default function App() { )} - +
{announcement} diff --git a/apps/web/src/components/AppFooter.tsx b/apps/web/src/components/AppFooter.tsx new file mode 100644 index 0000000..30a7e16 --- /dev/null +++ b/apps/web/src/components/AppFooter.tsx @@ -0,0 +1,79 @@ +// One centered line: libreble · GitHub · Report an issue · build version. Colours come from the +// zinc ramp, which index.css mirrors per theme; the libreble accent opts in via `dark:`. + +const REPO = 'https://github.com/libreble/multimeter'; +/** A clean tag build ("v1.2.0") links to its release; anything else is shown as plain text. */ +const RELEASE = /^v\d+\.\d+\.\d+$/.test(__APP_VERSION__) + ? `${REPO}/releases/tag/${__APP_VERSION__}` + : null; + +const LINK = 'inline-flex items-center gap-1.5 hover:text-zinc-300'; + +export function AppFooter() { + return ( + + ); +} + +/** The libreble "open beacon": an LED inside two rings opening to the top right. */ +function LibrebleMark() { + return ( + + ); +} + +function GitHubMark() { + return ( + + ); +} diff --git a/apps/web/src/components/ReportToast.test.tsx b/apps/web/src/components/ReportToast.test.tsx index ec1fbf4..9dcc50d 100644 --- a/apps/web/src/components/ReportToast.test.tsx +++ b/apps/web/src/components/ReportToast.test.tsx @@ -67,9 +67,46 @@ describe('ReportToast', () => { open.mockRestore(); unmount(); - // Remembered per driver: a later session doesn't ask again. + // Remembered per install: a later session doesn't ask again — not even for another driver. const again = render(); expect(again.container.firstChild).toBeNull(); + again.unmount(); + const other = render( + , + ); + expect(other.container.firstChild).toBeNull(); + }); + + it('asks once per install, even across drivers in one session', () => { + const { container, rerender } = render( + , + ); + fireEvent.click(screen.getByRole('button', { name: /dismiss/i })); + expect(container.firstChild).toBeNull(); + rerender(); + expect(container.firstChild).toBeNull(); + }); + + it('honours a per-driver key left by an older build', () => { + localStorage.setItem('multimeter.reportDone.ut181a', '1'); + const { container } = render( + , + ); + expect(container.firstChild).toBeNull(); + }); + + it('offers a problem report for an error surfaced while connected, re-armed by a new error', () => { + localStorage.setItem('multimeter.reportDone', '1'); + const live = (error: string | null) => + meters([channel({ state: 'live', driverId: 'uni-t', error })]); + const { container, rerender } = render(); + expect(screen.getByText(/trouble with your meter/i)).toBeInTheDocument(); + fireEvent.click(screen.getByRole('button', { name: /dismiss/i })); + expect(container.firstChild).toBeNull(); + rerender(); + expect(container.firstChild).toBeNull(); + rerender(); + expect(screen.getByText(/trouble with your meter/i)).toBeInTheDocument(); }); it('never asks about demo meters', () => { diff --git a/apps/web/src/components/ReportToast.tsx b/apps/web/src/components/ReportToast.tsx index 7ed2f0f..bcc0c7a 100644 --- a/apps/web/src/components/ReportToast.tsx +++ b/apps/web/src/components/ReportToast.tsx @@ -1,57 +1,58 @@ // A dismissible, non-blocking toast inviting a device report (see lib/report.ts). Two cases: // * a real meter is live on a driver not yet confirmed on hardware → "how is it working?". Dismissing -// or reporting is remembered per driver, so it asks once. -// * the chooser was dismissed or connecting failed → "trouble finding or connecting?". -// Dismissing hides it until the next cancel/failure. +// or reporting is remembered once per install, whatever the driver — a working meter shouldn't nag. +// * the chooser was dismissed or any error (connect, reconnect, or one surfaced while live) → +// "trouble with your meter?". Dismissing hides it until the next cancel/error. // Both open a pre-filled GitHub issue form in a new tab; the user reviews and submits it there. import { useEffect, useState } from 'react'; import type { Meters } from '@libreble/multimeter-react'; import { connectionProblemUrl, deviceReportUrl, verification } from '../lib/report'; -const DONE_KEY = (driverId: string) => `multimeter.reportDone.${driverId}`; +const DONE_KEY = 'multimeter.reportDone'; -function isDone(driverId: string): boolean { +function isDone(): boolean { try { - return localStorage.getItem(DONE_KEY(driverId)) !== null; + // Older builds stored one key per driver (multimeter.reportDone.) — honour those. + return Object.keys(localStorage).some(k => k === DONE_KEY || k.startsWith(`${DONE_KEY}.`)); } catch { return false; } } -function markDone(driverId: string): void { +function markDone(): void { try { - localStorage.setItem(DONE_KEY(driverId), '1'); + localStorage.setItem(DONE_KEY, '1'); } catch { /* storage unavailable — it may ask again next time */ } } export function ReportToast({ meters }: { meters: Meters }) { - const [done, setDone] = useState>(() => new Set()); + const [doneNow, setDoneNow] = useState(false); const [problemDismissed, setProblemDismissed] = useState(false); const real = meters.meters.filter(c => !meters.meterSession(c.id)?.isDemo); - const unconfirmed = real.find( - c => - c.state === 'live' && - c.driverId !== null && - verification(c.driverId) !== 'live-tested' && - !done.has(c.driverId) && - !isDone(c.driverId), - ); - const problem = real.find(c => c.cancelled || c.state === 'error'); + const unconfirmed = + !doneNow && !isDone() + ? real.find( + c => + c.state === 'live' && c.driverId !== null && verification(c.driverId) !== 'live-tested', + ) + : undefined; + const problem = real.find(c => c.cancelled || c.state === 'error' || c.error !== null); + // Which problem is showing: re-arm once it clears, or when a different error replaces it. + const problemKey = problem ? `${problem.id}\n${problem.error ?? 'cancelled'}` : null; - // A new cancel/failure after a successful attempt shows the toast again. useEffect(() => { - if (!problem) setProblemDismissed(false); - }, [problem]); + setProblemDismissed(false); + }, [problemKey]); if (unconfirmed) { const driverId = unconfirmed.driverId!; const finish = () => { - markDone(driverId); - setDone(d => new Set(d).add(driverId)); + markDone(); + setDoneNow(true); }; // Read the GATT description only on click: a few best-effort Device Information reads that // shouldn't sit in the connect path. Transient user activation outlives them, so the new tab @@ -74,7 +75,7 @@ export function ReportToast({ meters }: { meters: Meters }) { if (problem && !problemDismissed) { return ( setProblemDismissed(true)}> -

Trouble finding or connecting your meter?

+

Trouble with your meter?

+/// import { defineConfig } from 'vitest/config'; import { loadEnv } from 'vite'; import react from '@vitejs/plugin-react'; import tailwindcss from '@tailwindcss/vite'; import { VitePWA } from 'vite-plugin-pwa'; +import { execSync } from 'node:child_process'; // Web Bluetooth needs a secure context: localhost (dev) is fine; for phone testing // over the LAN you'd need HTTPS. `host: true` exposes the dev server on the network. @@ -12,6 +14,21 @@ import { VitePWA } from 'vite-plugin-pwa'; // `base` is the repo subpath and the service-worker scope + manifest start_url/scope all // mirror it. (Switch to '/' + a CNAME only if it ever moves to a dedicated custom domain.) // Self-hosters override it: `BASE_PATH=/ pnpm build` (the Docker image does this). +// Shown in the footer. Tag builds in CI use the tag (v1.2.0); elsewhere `git describe` +// (v1.2.0-3-gabc1234, or a bare sha on a shallow clone); `APP_VERSION` overrides; no git → "dev". +function appVersion(): string { + if (process.env.APP_VERSION) return process.env.APP_VERSION; + if (process.env.GITHUB_REF_TYPE === 'tag' && process.env.GITHUB_REF_NAME) + return process.env.GITHUB_REF_NAME; + try { + return execSync('git describe --tags --always', { stdio: ['ignore', 'pipe', 'ignore'] }) + .toString() + .trim(); + } catch { + return 'dev'; + } +} + export default defineConfig(({ mode }) => { const env = loadEnv(mode, '.', ''); const base = `/${(env.BASE_PATH || '/multimeter/').replace(/^\/+|\/+$/g, '')}/`.replace( @@ -20,6 +37,7 @@ export default defineConfig(({ mode }) => { ); return { base, + define: { __APP_VERSION__: JSON.stringify(appVersion()) }, plugins: [ react(), tailwindcss(), diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8c72b0a..9982e6c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -46,7 +46,7 @@ importers: version: 8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3) vitest: specifier: ^4.1.8 - version: 4.1.8(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1)(vite@6.4.3(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)) + version: 4.1.8(@types/node@26.6.2)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1)(vite@6.4.3(@types/node@26.6.2)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)) apps/web: dependencies: @@ -77,7 +77,7 @@ importers: version: 10.0.1(eslint@10.4.1(jiti@2.7.0)) '@tailwindcss/vite': specifier: ^4.0.0 - version: 4.3.0(vite@6.4.3(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)) + version: 4.3.0(vite@6.4.3(@types/node@26.6.2)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)) '@testing-library/dom': specifier: ^10.4.1 version: 10.4.1 @@ -90,6 +90,9 @@ importers: '@testing-library/user-event': specifier: ^14.6.1 version: 14.6.1(@testing-library/dom@10.4.1) + '@types/node': + specifier: ^26.6.2 + version: 26.6.2 '@types/react': specifier: ^19.0.0 version: 19.2.17 @@ -104,7 +107,7 @@ importers: version: 1.0.2 '@vitejs/plugin-react': specifier: ^4.3.4 - version: 4.7.0(vite@6.4.3(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)) + version: 4.7.0(vite@6.4.3(@types/node@26.6.2)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)) '@vitest/coverage-v8': specifier: ^4.1.8 version: 4.1.8(vitest@4.1.8) @@ -140,13 +143,13 @@ importers: version: 8.60.1(eslint@10.4.1(jiti@2.7.0))(typescript@5.9.3) vite: specifier: ^6.0.0 - version: 6.4.3(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0) + version: 6.4.3(@types/node@26.6.2)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0) vite-plugin-pwa: specifier: ^1.3.0 - version: 1.3.0(@vite-pwa/assets-generator@1.0.2)(vite@6.4.3(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0))(workbox-build@7.4.1(@types/babel__core@7.20.5))(workbox-window@7.4.1) + version: 1.3.0(@vite-pwa/assets-generator@1.0.2)(vite@6.4.3(@types/node@26.6.2)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0))(workbox-build@7.4.1(@types/babel__core@7.20.5))(workbox-window@7.4.1) vitest: specifier: ^4.1.8 - version: 4.1.8(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1)(vite@6.4.3(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)) + version: 4.1.8(@types/node@26.6.2)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1)(vite@6.4.3(@types/node@26.6.2)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)) packages/protocol: devDependencies: @@ -158,7 +161,7 @@ importers: version: 5.9.3 vitest: specifier: ^4.1.8 - version: 4.1.8(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1)(vite@6.4.3(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)) + version: 4.1.8(@types/node@26.6.2)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1)(vite@6.4.3(@types/node@26.6.2)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)) packages/react: dependencies: @@ -204,7 +207,7 @@ importers: version: 5.9.3 vitest: specifier: ^4.1.8 - version: 4.1.8(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1)(vite@6.4.3(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)) + version: 4.1.8(@types/node@26.6.2)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1)(vite@6.4.3(@types/node@26.6.2)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)) packages/recorder: dependencies: @@ -223,7 +226,7 @@ importers: version: 5.9.3 vitest: specifier: ^4.1.8 - version: 4.1.8(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1)(vite@6.4.3(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)) + version: 4.1.8(@types/node@26.6.2)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1)(vite@6.4.3(@types/node@26.6.2)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)) packages/vue: dependencies: @@ -254,7 +257,7 @@ importers: version: 5.9.3 vitest: specifier: ^4.1.8 - version: 4.1.8(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1)(vite@6.4.3(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)) + version: 4.1.8(@types/node@26.6.2)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1)(vite@6.4.3(@types/node@26.6.2)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)) vue: specifier: ^3.5.13 version: 3.5.35(typescript@5.9.3) @@ -279,7 +282,7 @@ importers: version: 5.9.3 vitest: specifier: ^4.1.8 - version: 4.1.8(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1)(vite@6.4.3(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)) + version: 4.1.8(@types/node@26.6.2)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1)(vite@6.4.3(@types/node@26.6.2)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)) packages: @@ -1724,6 +1727,9 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/node@26.6.2': + resolution: {integrity: sha512-X1P21scMv4zGKLYqjdGjaKa7COa0RKVYYZZN/NfvLQ1JegxFhdhpZG/Lyn8AXx6CDUavKAd11v6BvfpkDByK8g==} + '@types/react-dom@19.2.3': resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} peerDependencies: @@ -3381,6 +3387,9 @@ packages: unconfig@7.5.0: resolution: {integrity: sha512-oi8Qy2JV4D3UQ0PsopR28CzdQ3S/5A1zwsUwp/rosSbfhJ5z7b90bIyTwi/F7hCLD4SGcZVjDzd4XoUQcEanvA==} + undici-types@8.9.0: + resolution: {integrity: sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==} + undici@7.27.2: resolution: {integrity: sha512-uZsKNuzQxDMUY6M3pIMvy5tvlGmtq8XJ2oLAkfRKGNu+1VQAIvLy2xIVG5ATZl5wDXl/tddByAWCizRbOme+TA==} engines: {node: '>=20.18.1'} @@ -4893,12 +4902,12 @@ snapshots: '@tailwindcss/oxide-win32-arm64-msvc': 4.3.0 '@tailwindcss/oxide-win32-x64-msvc': 4.3.0 - '@tailwindcss/vite@4.3.0(vite@6.4.3(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0))': + '@tailwindcss/vite@4.3.0(vite@6.4.3(@types/node@26.6.2)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0))': dependencies: '@tailwindcss/node': 4.3.0 '@tailwindcss/oxide': 4.3.0 tailwindcss: 4.3.0 - vite: 6.4.3(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0) + vite: 6.4.3(@types/node@26.6.2)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0) '@testing-library/dom@10.4.1': dependencies: @@ -4977,6 +4986,10 @@ snapshots: '@types/json-schema@7.0.15': {} + '@types/node@26.6.2': + dependencies: + undici-types: 8.9.0 + '@types/react-dom@19.2.3(@types/react@19.2.17)': dependencies: '@types/react': 19.2.17 @@ -5091,7 +5104,7 @@ snapshots: sharp-ico: 0.1.5 unconfig: 7.5.0 - '@vitejs/plugin-react@4.7.0(vite@6.4.3(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0))': + '@vitejs/plugin-react@4.7.0(vite@6.4.3(@types/node@26.6.2)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0))': dependencies: '@babel/core': 7.29.7 '@babel/plugin-transform-react-jsx-self': 7.29.7(@babel/core@7.29.7) @@ -5099,7 +5112,7 @@ snapshots: '@rolldown/pluginutils': 1.0.0-beta.27 '@types/babel__core': 7.20.5 react-refresh: 0.17.0 - vite: 6.4.3(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0) + vite: 6.4.3(@types/node@26.6.2)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0) transitivePeerDependencies: - supports-color @@ -5115,7 +5128,7 @@ snapshots: obug: 2.1.2 std-env: 4.1.0 tinyrainbow: 3.1.0 - vitest: 4.1.8(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1)(vite@6.4.3(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)) + vitest: 4.1.8(@types/node@26.6.2)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1)(vite@6.4.3(@types/node@26.6.2)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)) '@vitest/expect@4.1.8': dependencies: @@ -5126,13 +5139,13 @@ snapshots: chai: 6.2.2 tinyrainbow: 3.1.0 - '@vitest/mocker@4.1.8(vite@6.4.3(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0))': + '@vitest/mocker@4.1.8(vite@6.4.3(@types/node@26.6.2)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0))': dependencies: '@vitest/spy': 4.1.8 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 6.4.3(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0) + vite: 6.4.3(@types/node@26.6.2)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0) '@vitest/pretty-format@4.1.8': dependencies: @@ -6877,6 +6890,8 @@ snapshots: quansync: 1.0.0 unconfig-core: 7.5.0 + undici-types@8.9.0: {} + undici@7.27.2: {} unicode-canonical-property-names-ecmascript@2.0.1: {} @@ -6910,12 +6925,12 @@ snapshots: dependencies: punycode: 2.3.1 - vite-plugin-pwa@1.3.0(@vite-pwa/assets-generator@1.0.2)(vite@6.4.3(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0))(workbox-build@7.4.1(@types/babel__core@7.20.5))(workbox-window@7.4.1): + vite-plugin-pwa@1.3.0(@vite-pwa/assets-generator@1.0.2)(vite@6.4.3(@types/node@26.6.2)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0))(workbox-build@7.4.1(@types/babel__core@7.20.5))(workbox-window@7.4.1): dependencies: debug: 4.4.3 pretty-bytes: 6.1.1 tinyglobby: 0.2.17 - vite: 6.4.3(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0) + vite: 6.4.3(@types/node@26.6.2)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0) workbox-build: 7.4.1(@types/babel__core@7.20.5) workbox-window: 7.4.1 optionalDependencies: @@ -6923,7 +6938,7 @@ snapshots: transitivePeerDependencies: - supports-color - vite@6.4.3(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0): + vite@6.4.3(@types/node@26.6.2)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0): dependencies: esbuild: 0.25.12 fdir: 6.5.0(picomatch@4.0.4) @@ -6932,15 +6947,16 @@ snapshots: rollup: 4.61.1 tinyglobby: 0.2.17 optionalDependencies: + '@types/node': 26.6.2 fsevents: 2.3.3 jiti: 2.7.0 lightningcss: 1.32.0 terser: 5.48.0 - vitest@4.1.8(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1)(vite@6.4.3(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)): + vitest@4.1.8(@types/node@26.6.2)(@vitest/coverage-v8@4.1.8)(jsdom@29.1.1)(vite@6.4.3(@types/node@26.6.2)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)): dependencies: '@vitest/expect': 4.1.8 - '@vitest/mocker': 4.1.8(vite@6.4.3(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)) + '@vitest/mocker': 4.1.8(vite@6.4.3(@types/node@26.6.2)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0)) '@vitest/pretty-format': 4.1.8 '@vitest/runner': 4.1.8 '@vitest/snapshot': 4.1.8 @@ -6957,9 +6973,10 @@ snapshots: tinyexec: 1.2.4 tinyglobby: 0.2.17 tinyrainbow: 3.1.0 - vite: 6.4.3(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0) + vite: 6.4.3(@types/node@26.6.2)(jiti@2.7.0)(lightningcss@1.32.0)(terser@5.48.0) why-is-node-running: 2.3.0 optionalDependencies: + '@types/node': 26.6.2 '@vitest/coverage-v8': 4.1.8(vitest@4.1.8) jsdom: 29.1.1 transitivePeerDependencies: