refactor: replace hand-rolled debounce with @tanstack/react-pacer and extract useGracePeriod

- Add `@tanstack/react-pacer` and swap both hand-rolled `setTimeout`-based debounce patterns (project-name commit in `project-setup.tsx`, module search fetch in `site-search.tsx`) for `useDebouncedValue`; the search effect now runs directly against the debounced value with an abort controller rather than nesting the fetch inside the timer
- Extract `useGracePeriod` from `file-preview.tsx` into `src/hooks/use-grace-period.ts` so it can be reused independently
- Hoist `fileSelection`, `lastFileRef`, `activePath`, and `preview` above the reseed effect in `file-preview.tsx` so the effect can reference `defaultPath` and `lastFileRef` without stale-closure issues; expand ancestor directories when a hash-driven selection lands so the highlighted row is visible in the tree
This commit is contained in:
jake committed 2026-05-25 14:23:33 -04:00
1 parent 16295952a8
commit 6767362641
6 files changed
+149 -202

No files matched your search

+1
View File
@@ -23,6 +23,7 @@
"@takumi-rs/image-response": "^1.5.1", "@takumi-rs/image-response": "^1.5.1",
"@tanstack/react-devtools": "^0.10.5", "@tanstack/react-devtools": "^0.10.5",
"@tanstack/react-hotkeys": "^0.10.0", "@tanstack/react-hotkeys": "^0.10.0",
"@tanstack/react-pacer": "^0.22.1",
"@tanstack/react-router": "^1.170.8", "@tanstack/react-router": "^1.170.8",
"@tanstack/react-router-devtools": "^1.167.0", "@tanstack/react-router-devtools": "^1.167.0",
"@tanstack/react-router-ssr-query": "^1.167.0", "@tanstack/react-router-ssr-query": "^1.167.0",
@@ -3,11 +3,12 @@ import { FileTree, useFileTree, useFileTreeSelector } from "@pierre/trees/react"
import { IconLoader2 } from "@tabler/icons-react"; import { IconLoader2 } from "@tabler/icons-react";
import { useLocation, useNavigate } from "@tanstack/react-router"; import { useLocation, useNavigate } from "@tanstack/react-router";
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import { useEffect, useMemo, useRef, useState } from "react"; import { useEffect, useMemo, useRef } from "react";
import { useTheme } from "@/components/theme-provider"; import { useTheme } from "@/components/theme-provider";
import { Card } from "@/components/ui/card"; import { Card } from "@/components/ui/card";
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable"; import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable";
import { useGracePeriod } from "@/hooks/use-grace-period";
import { useMediaQuery } from "@/hooks/use-media-query"; import { useMediaQuery } from "@/hooks/use-media-query";
import type { Preview } from "@/server/highlighter"; import type { Preview } from "@/server/highlighter";
@@ -33,17 +34,17 @@ const TRUNCATE_FIX_CSS = `
display: none !important; display: none !important;
}`; }`;
// Prefer the root `package.json` — best at-a-glance summary of the stack.
function defaultPathFor(filePaths: string[]): string | undefined {
return filePaths.includes("package.json") ? "package.json" : filePaths[0];
}
// Mirror TanStack Router's `defaultPendingMs` / `defaultPendingMinMs`: wait // Mirror TanStack Router's `defaultPendingMs` / `defaultPendingMinMs`: wait
// before showing a pending indicator (skip the flash for fast loads), and once // before showing a pending indicator (skip the flash for fast loads), and once
// shown, keep it visible at least this long (avoid flicker on near-misses). // shown, keep it visible at least this long (avoid flicker on near-misses).
const OVERLAY_PENDING_MS = 300; const OVERLAY_PENDING_MS = 300;
const OVERLAY_MIN_MS = 250; const OVERLAY_MIN_MS = 250;
// Prefer the root `package.json` — best at-a-glance summary of the stack.
function defaultPathFor(filePaths: string[]): string | undefined {
return filePaths.includes("package.json") ? "package.json" : filePaths[0];
}
// "a/b/c.ts" → ["a", "a/b"]. // "a/b/c.ts" → ["a", "a/b"].
function directoryPaths(paths: readonly string[]): string[] { function directoryPaths(paths: readonly string[]): string[] {
const dirs = new Set<string>(); const dirs = new Set<string>();
@@ -59,30 +60,6 @@ function directoryPaths(paths: readonly string[]): string[] {
return [...dirs]; return [...dirs];
} }
// Defer the truthy edge by `delayMs`, then hold true for at least `minMs`.
function useGracePeriod(active: boolean, delayMs: number, minMs: number): boolean {
const [visible, setVisible] = useState(false);
const shownAtRef = useRef(0);
useEffect(() => {
if (active && !visible) {
const t = setTimeout(() => {
shownAtRef.current = performance.now();
setVisible(true);
}, delayMs);
return () => clearTimeout(t);
}
if (!active && visible) {
const remaining = Math.max(0, minMs - (performance.now() - shownAtRef.current));
const t = setTimeout(() => setVisible(false), remaining);
return () => clearTimeout(t);
}
return undefined;
}, [active, visible, delayMs, minMs]);
return visible;
}
export function FilePreview({ export function FilePreview({
filePaths, filePaths,
previews, previews,
@@ -113,46 +90,7 @@ export function FilePreview({
const hashRef = useRef(hash); const hashRef = useRef(hash);
hashRef.current = hash; hashRef.current = hash;
const navigate = useNavigate({ from: "/" }); const navigate = useNavigate({ from: "/" });
const defaultPath = useMemo(() => defaultPathFor(filePaths), [filePaths]);
// Re-seed the model when `filePaths` changes. `resetPaths` collapses
// everything and clears selection, so we replay both manually.
const prevPathsRef = useRef(filePaths);
useEffect(() => {
const expandedSet = new Set(
directoryPaths(prevPathsRef.current).filter((dir) => {
const item = model.getItem(dir);
return item != null && "isExpanded" in item && item.isExpanded();
}),
);
// Collapsing a directory leaves its descendants flagged expanded.
// `initialExpandedPaths` re-expands the full ancestor chain of every path
// given, so replaying such a descendant would re-open the just-collapsed
// parent. Keep only dirs whose entire ancestor chain is still expanded.
const expanded = [...expandedSet].filter((dir) => {
const segments = dir.split("/");
segments.pop();
let prefix = "";
for (const segment of segments) {
prefix = prefix ? `${prefix}/${segment}` : segment;
if (!expandedSet.has(prefix)) return false;
}
return true;
});
expanded.sort();
const previousSelection = model.getSelectedPaths()[0];
model.resetPaths(filePaths, { initialExpandedPaths: expanded });
// Prefer the hash (deep link / refresh), then the prior selection, then
// the default. The hash is read via ref so its changes don't reseed.
const initialHash = hashRef.current;
const next =
initialHash && filePaths.includes(initialHash)
? initialHash
: previousSelection != null && filePaths.includes(previousSelection)
? previousSelection
: defaultPathFor(filePaths);
if (next) model.getItem(next)?.select();
prevPathsRef.current = filePaths;
}, [model, filePaths]);
// Project to "current file selection (undefined for nothing/folder)". The // Project to "current file selection (undefined for nothing/folder)". The
// selector pattern lets the tree skip re-rendering us on unrelated state // selector pattern lets the tree skip re-rendering us on unrelated state
@@ -171,15 +109,64 @@ export function FilePreview({
lastFileRef.current = undefined; lastFileRef.current = undefined;
} }
if (fileSelection !== undefined) lastFileRef.current = fileSelection; if (fileSelection !== undefined) lastFileRef.current = fileSelection;
const activePath = lastFileRef.current ?? defaultPathFor(filePaths); const activePath = lastFileRef.current ?? defaultPath;
const preview = activePath ? previews[activePath] : undefined; const preview = activePath ? previews[activePath] : undefined;
// Re-seed the model when `filePaths` changes. `resetPaths` collapses
// everything and clears selection, so we replay both manually.
const prevPathsRef = useRef(filePaths);
useEffect(() => {
const expandedSet = new Set(
directoryPaths(prevPathsRef.current).filter((dir) => {
const item = model.getItem(dir);
return item != null && "isExpanded" in item && item.isExpanded();
}),
);
// Collapsing a directory leaves its descendants flagged expanded.
// `initialExpandedPaths` re-expands the full ancestor chain of every path
// given, so replaying such a descendant would re-open the just-collapsed
// parent. Keep only dirs whose entire ancestor chain is still expanded.
const preservedExpansion = [...expandedSet].filter((dir) => {
const segments = dir.split("/");
segments.pop();
let prefix = "";
for (const segment of segments) {
prefix = prefix ? `${prefix}/${segment}` : segment;
if (!expandedSet.has(prefix)) return false;
}
return true;
});
// Prefer the hash (deep link / refresh), then the latched last file,
// then the default. Hash is read via ref so its changes don't reseed.
const initialHash = hashRef.current;
const previous = lastFileRef.current;
const next =
initialHash && filePaths.includes(initialHash)
? initialHash
: previous && filePaths.includes(previous)
? previous
: defaultPath;
// Reveal the selected file by expanding its ancestor chain — otherwise
// the tree selects a row that's collapsed out of view.
const expanded = [
...new Set([...preservedExpansion, ...(next ? directoryPaths([next]) : [])]),
].toSorted();
model.resetPaths(filePaths, { initialExpandedPaths: expanded });
if (next) model.getItem(next)?.select();
prevPathsRef.current = filePaths;
}, [model, filePaths, defaultPath]);
// URL → tree: back/forward (and intra-app links) drop a new hash; reflect // URL → tree: back/forward (and intra-app links) drop a new hash; reflect
// it in the tree. Skipped on the initial reseed since that effect already // it in the tree. Skipped on the initial reseed since that effect already
// honors the hash via `hashRef`. // honors the hash via `hashRef`.
useEffect(() => { useEffect(() => {
if (!hash || !filePaths.includes(hash)) return; if (!hash || !filePaths.includes(hash)) return;
if (model.getSelectedPaths()[0] === hash) return; if (model.getSelectedPaths()[0] === hash) return;
// Expand ancestors so the just-selected row is actually visible.
for (const dir of directoryPaths([hash])) {
const item = model.getItem(dir);
if (item && "expand" in item) item.expand();
}
model.getItem(hash)?.select(); model.getItem(hash)?.select();
}, [hash, filePaths, model]); }, [hash, filePaths, model]);
@@ -189,9 +176,7 @@ export function FilePreview({
// cleared after the owning module is deselected). // cleared after the owning module is deselected).
useEffect(() => { useEffect(() => {
const nextHash = const nextHash =
activePath && filePaths.includes(activePath) && activePath !== defaultPathFor(filePaths) activePath && filePaths.includes(activePath) && activePath !== defaultPath ? activePath : "";
? activePath
: "";
if (nextHash === hash) return; if (nextHash === hash) return;
void navigate({ void navigate({
search: (prev) => prev, search: (prev) => prev,
@@ -200,7 +185,7 @@ export function FilePreview({
resetScroll: false, resetScroll: false,
hashScrollIntoView: false, hashScrollIntoView: false,
}); });
}, [activePath, hash, filePaths, navigate]); }, [activePath, hash, filePaths, defaultPath, navigate]);
// Drive the tree's palette from the app theme; otherwise it auto-detects via // Drive the tree's palette from the app theme; otherwise it auto-detects via
// `prefers-color-scheme` and mismatches when the user overrides the OS theme. // `prefers-color-scheme` and mismatches when the user overrides the OS theme.
@@ -1,7 +1,8 @@
import { validateProjectName } from "@stanza/registry"; import { validateProjectName } from "@stanza/registry";
import { IconAlertCircle } from "@tabler/icons-react"; import { IconAlertCircle } from "@tabler/icons-react";
import { useDebouncedValue } from "@tanstack/react-pacer";
import type { ChangeEvent } from "react"; import type { ChangeEvent } from "react";
import { useCallback, useEffect, useEffectEvent, useId, useMemo, useRef, useState } from "react"; import { useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
import { Card } from "@/components/ui/card"; import { Card } from "@/components/ui/card";
import { Field, FieldError } from "@/components/ui/field"; import { Field, FieldError } from "@/components/ui/field";
@@ -17,10 +18,9 @@ export function ProjectSetup({
defaultName: string; defaultName: string;
onNameChange: (name: string) => void; onNameChange: (name: string) => void;
}) { }) {
// Debounce the upward push (navigates → reruns the loader) while keeping the // `lastSyncedRef` ignores the echo of our own pushes — otherwise
// field responsive. `lastSyncedRef` ignores the echo of our own pushes — // `setDraft(name)` would clobber characters the user typed between the
// otherwise `setDraft(name)` would clobber characters the user typed between // debounce firing and the new `name` prop arriving.
// the debounce firing and the new `name` prop arriving.
const [draft, setDraft] = useState(name); const [draft, setDraft] = useState(name);
const lastSyncedRef = useRef(name); const lastSyncedRef = useRef(name);
@@ -38,19 +38,14 @@ export function ProjectSetup({
// shouldn't render a "required" error. // shouldn't render a "required" error.
const showError = !validation.ok && draft.trim().length > 0; const showError = !validation.ok && draft.trim().length > 0;
// Read the latest `onNameChange` without making the debounce effect re-run const [debouncedDraft] = useDebouncedValue(draft, { wait: 300 });
// every time the parent rebuilds the callback.
const commit = useEffectEvent((next: string) => {
lastSyncedRef.current = next;
onNameChange(next);
});
useEffect(() => { useEffect(() => {
if (draft === lastSyncedRef.current) return undefined; if (debouncedDraft === lastSyncedRef.current) return;
if (!validation.ok) return undefined; if (!validateProjectName(debouncedDraft).ok) return;
const timer = setTimeout(() => commit(draft), 300); lastSyncedRef.current = debouncedDraft;
return () => clearTimeout(timer); onNameChange(debouncedDraft);
}, [draft, validation.ok]); }, [debouncedDraft, onNameChange]);
const onDraftChange = useCallback( const onDraftChange = useCallback(
(e: ChangeEvent<HTMLInputElement>) => setDraft(e.target.value), (e: ChangeEvent<HTMLInputElement>) => setDraft(e.target.value),
+18 -18
View File
@@ -4,6 +4,7 @@ import type { ModuleSummary, RegistryIndex } from "@stanza/registry";
import { categoryLabel } from "@stanza/registry"; import { categoryLabel } from "@stanza/registry";
import { IconBookmark, IconSearch } from "@tabler/icons-react"; import { IconBookmark, IconSearch } from "@tabler/icons-react";
import { formatForDisplay, useHotkey } from "@tanstack/react-hotkeys"; import { formatForDisplay, useHotkey } from "@tanstack/react-hotkeys";
import { useDebouncedValue } from "@tanstack/react-pacer";
import { useNavigate } from "@tanstack/react-router"; import { useNavigate } from "@tanstack/react-router";
import type { SortedResult } from "fumadocs-core/search"; import type { SortedResult } from "fumadocs-core/search";
import { useDocsSearch } from "fumadocs-core/search/client"; import { useDocsSearch } from "fumadocs-core/search/client";
@@ -76,30 +77,29 @@ export function SiteSearch({ registry, docs }: { registry: RegistryIndex; docs:
docsSearch.setSearch(query); docsSearch.setSearch(query);
}, [query, docsSearch]); }, [query, docsSearch]);
// Modules: small hand-rolled debounced fetch (not worth abstracting). // Modules: debounce the query, then fetch with an abort signal for any
// request that's still in-flight when the debounced value changes again.
const [moduleResults, setModuleResults] = useState<ModuleSummary[] | null>(null); const [moduleResults, setModuleResults] = useState<ModuleSummary[] | null>(null);
const [debouncedQuery] = useDebouncedValue(query, { wait: DEBOUNCE_MS });
useEffect(() => { useEffect(() => {
if (!query.trim()) { if (!debouncedQuery.trim()) {
setModuleResults(null); setModuleResults(null);
return () => {}; return () => {};
} }
const controller = new AbortController(); const controller = new AbortController();
const timer = setTimeout(() => { fetch(`/api/search/modules?q=${encodeURIComponent(debouncedQuery)}`, {
fetch(`/api/search/modules?q=${encodeURIComponent(query)}`, { signal: controller.signal }) signal: controller.signal,
.then((res) => res.json()) })
.then((data: unknown) => setModuleResults(parseModuleResults(data))) .then((res) => res.json())
.catch((err: unknown) => { .then((data: unknown) => setModuleResults(parseModuleResults(data)))
if (err instanceof DOMException && err.name === "AbortError") return; .catch((err: unknown) => {
// Surface network/parse failures as "no matches" rather than blanking if (err instanceof DOMException && err.name === "AbortError") return;
// the dialog or throwing past React's render loop. // Surface network/parse failures as "no matches" rather than blanking
setModuleResults([]); // the dialog or throwing past React's render loop.
}); setModuleResults([]);
}, DEBOUNCE_MS); });
return () => { return () => controller.abort();
clearTimeout(timer); }, [debouncedQuery]);
controller.abort();
};
}, [query]);
const pageTitlesByUrl = useMemo( const pageTitlesByUrl = useMemo(
() => new Map(docs.pages.map((p) => [p.url, p.title])), () => new Map(docs.pages.map((p) => [p.url, p.title])),
+31
View File
@@ -0,0 +1,31 @@
import { useEffect, useRef, useState } from "react";
/**
* Defer the truthy edge by `delayMs`, then hold true for at least `minMs`.
*
* Mirrors TanStack Router's `defaultPendingMs` / `defaultPendingMinMs`: wait
* before showing a pending indicator (skip the flash for fast loads), and once
* shown, keep it visible at least this long (avoid flicker on near-misses).
*/
export function useGracePeriod(active: boolean, delayMs: number, minMs: number): boolean {
const [visible, setVisible] = useState(false);
const shownAtRef = useRef(0);
useEffect(() => {
if (active && !visible) {
const t = setTimeout(() => {
shownAtRef.current = performance.now();
setVisible(true);
}, delayMs);
return () => clearTimeout(t);
}
if (!active && visible) {
const remaining = Math.max(0, minMs - (performance.now() - shownAtRef.current));
const t = setTimeout(() => setVisible(false), remaining);
return () => clearTimeout(t);
}
return undefined;
}, [active, visible, delayMs, minMs]);
return visible;
}
+28 -93
View File
@@ -121,6 +121,9 @@ importers:
'@tanstack/react-hotkeys': '@tanstack/react-hotkeys':
specifier: ^0.10.0 specifier: ^0.10.0
version: 0.10.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) version: 0.10.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
'@tanstack/react-pacer':
specifier: ^0.22.1
version: 0.22.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
'@tanstack/react-router': '@tanstack/react-router':
specifier: ^1.170.8 specifier: ^1.170.8
version: 1.170.8(react-dom@19.2.6(react@19.2.6))(react@19.2.6) version: 1.170.8(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
@@ -241,10 +244,10 @@ importers:
version: '@voidzero-dev/vite-plus-core@0.1.22(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(typescript@6.0.3)' version: '@voidzero-dev/vite-plus-core@0.1.22(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(typescript@6.0.3)'
vite-plus: vite-plus:
specifier: 'catalog:' specifier: 'catalog:'
version: 0.1.22(@opentelemetry/api@1.9.1)(@types/node@25.9.1)(@voidzero-dev/vite-plus-core@0.1.22(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(typescript@6.0.3))(esbuild@0.28.0)(jiti@2.7.0)(jsdom@29.1.1(@noble/hashes@1.8.0))(typescript@6.0.3) version: 0.1.22(@opentelemetry/api@1.9.1)(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(jsdom@29.1.1(@noble/hashes@1.8.0))(typescript@6.0.3)(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0))
vitest: vitest:
specifier: npm:@voidzero-dev/vite-plus-test@^0.1.22 specifier: npm:@voidzero-dev/vite-plus-test@^0.1.22
version: '@voidzero-dev/vite-plus-test@0.1.22(@opentelemetry/api@1.9.1)(@types/node@25.9.1)(@voidzero-dev/vite-plus-core@0.1.22(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(typescript@6.0.3))(esbuild@0.28.0)(jiti@2.7.0)(jsdom@29.1.1(@noble/hashes@1.8.0))(typescript@6.0.3)' version: '@voidzero-dev/vite-plus-test@0.1.22(@opentelemetry/api@1.9.1)(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(jsdom@29.1.1(@noble/hashes@1.8.0))(typescript@6.0.3)(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0))'
web-vitals: web-vitals:
specifier: ^5.2.0 specifier: ^5.2.0
version: 5.2.0 version: 5.2.0
@@ -2014,6 +2017,10 @@ packages:
resolution: {integrity: sha512-vqH7X9nb0MTJ/O08++dB5bP9jgj4+BIPOUu/U+6myG86lDsirZSVSobpq5UQpE7nBuk62i8eIYeOhd+OMl/UrA==} resolution: {integrity: sha512-vqH7X9nb0MTJ/O08++dB5bP9jgj4+BIPOUu/U+6myG86lDsirZSVSobpq5UQpE7nBuk62i8eIYeOhd+OMl/UrA==}
engines: {node: '>=18'} engines: {node: '>=18'}
'@tanstack/pacer@0.21.1':
resolution: {integrity: sha512-hB01dd4rlsYcTCNP7wK186jgAe6K5qimgM1Y5Jtvz+9PUaILvpmeLLjmQNUNSO1l23lIt+CeQR6mO1mjlPvRtQ==}
engines: {node: '>=18'}
'@tanstack/query-core@5.100.14': '@tanstack/query-core@5.100.14':
resolution: {integrity: sha512-5X41dGpxgeaHISCRW2oYwcSycZeULZzAunaudXT9ov1KOTj9xwt0CH6hbwqP1/z74ZWF7rYFnDpyYH07XFcZew==} resolution: {integrity: sha512-5X41dGpxgeaHISCRW2oYwcSycZeULZzAunaudXT9ov1KOTj9xwt0CH6hbwqP1/z74ZWF7rYFnDpyYH07XFcZew==}
@@ -2033,6 +2040,13 @@ packages:
react: '>=16.8' react: '>=16.8'
react-dom: '>=16.8' react-dom: '>=16.8'
'@tanstack/react-pacer@0.22.1':
resolution: {integrity: sha512-CenQqK0GluSPIrnsG1yuD7w5uMSQ/4lI9AcGEFxBrRd66r260boWcYRIsS5+eHtXb238FoZYhKmJPGlhRzmHRw==}
engines: {node: '>=18'}
peerDependencies:
react: '>=16.8'
react-dom: '>=16.8'
'@tanstack/react-query@5.100.14': '@tanstack/react-query@5.100.14':
resolution: {integrity: sha512-oOr6aRdSFEwWhzxEkD/9ZcItM3+LjBSkeVmadWKwUssAHTsqd/7bOjWrX4AbvEkoEhgAxzN0Xk6H/aYzXiYBAw==} resolution: {integrity: sha512-oOr6aRdSFEwWhzxEkD/9ZcItM3+LjBSkeVmadWKwUssAHTsqd/7bOjWrX4AbvEkoEhgAxzN0Xk6H/aYzXiYBAw==}
peerDependencies: peerDependencies:
@@ -6646,6 +6660,11 @@ snapshots:
dependencies: dependencies:
'@tanstack/store': 0.11.0 '@tanstack/store': 0.11.0
'@tanstack/pacer@0.21.1':
dependencies:
'@tanstack/devtools-event-client': 0.4.3
'@tanstack/store': 0.11.0
'@tanstack/query-core@5.100.14': {} '@tanstack/query-core@5.100.14': {}
'@tanstack/react-devtools@0.10.5(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(csstype@3.2.3)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(solid-js@1.9.13)': '@tanstack/react-devtools@0.10.5(@types/react-dom@19.2.3(@types/react@19.2.15))(@types/react@19.2.15)(csstype@3.2.3)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(solid-js@1.9.13)':
@@ -6668,6 +6687,13 @@ snapshots:
react: 19.2.6 react: 19.2.6
react-dom: 19.2.6(react@19.2.6) react-dom: 19.2.6(react@19.2.6)
'@tanstack/react-pacer@0.22.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6)':
dependencies:
'@tanstack/pacer': 0.21.1
'@tanstack/react-store': 0.11.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
react: 19.2.6
react-dom: 19.2.6(react@19.2.6)
'@tanstack/react-query@5.100.14(react@19.2.6)': '@tanstack/react-query@5.100.14(react@19.2.6)':
dependencies: dependencies:
'@tanstack/query-core': 5.100.14 '@tanstack/query-core': 5.100.14
@@ -7076,48 +7102,6 @@ snapshots:
'@voidzero-dev/vite-plus-linux-x64-musl@0.1.22': '@voidzero-dev/vite-plus-linux-x64-musl@0.1.22':
optional: true optional: true
'@voidzero-dev/vite-plus-test@0.1.22(@opentelemetry/api@1.9.1)(@types/node@25.9.1)(@voidzero-dev/vite-plus-core@0.1.22(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(typescript@6.0.3))(esbuild@0.28.0)(jiti@2.7.0)(jsdom@29.1.1(@noble/hashes@1.8.0))(typescript@6.0.3)':
dependencies:
'@standard-schema/spec': 1.1.0
'@types/chai': 5.2.3
'@voidzero-dev/vite-plus-core': 0.1.22(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(typescript@6.0.3)
es-module-lexer: 1.7.0
obug: 2.1.1
pixelmatch: 7.2.0
pngjs: 7.0.0
sirv: 3.0.2
std-env: 4.1.0
tinybench: 2.9.0
tinyexec: 1.2.2
tinyglobby: 0.2.16
vite: '@voidzero-dev/vite-plus-core@0.1.22(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(typescript@6.0.3)'
ws: 8.21.0
optionalDependencies:
'@opentelemetry/api': 1.9.1
'@types/node': 25.9.1
jsdom: 29.1.1(@noble/hashes@1.8.0)
transitivePeerDependencies:
- '@arethetypeswrong/core'
- '@tsdown/css'
- '@tsdown/exe'
- '@vitejs/devtools'
- bufferutil
- esbuild
- jiti
- less
- publint
- sass
- sass-embedded
- stylus
- sugarss
- terser
- tsx
- typescript
- unplugin-unused
- unrun
- utf-8-validate
- yaml
'@voidzero-dev/vite-plus-test@0.1.22(@opentelemetry/api@1.9.1)(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(jsdom@29.1.1(@noble/hashes@1.8.0))(typescript@6.0.3)(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0))': '@voidzero-dev/vite-plus-test@0.1.22(@opentelemetry/api@1.9.1)(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(jsdom@29.1.1(@noble/hashes@1.8.0))(typescript@6.0.3)(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0))':
dependencies: dependencies:
'@standard-schema/spec': 1.1.0 '@standard-schema/spec': 1.1.0
@@ -9973,55 +9957,6 @@ snapshots:
'@types/unist': 3.0.3 '@types/unist': 3.0.3
vfile-message: 4.0.3 vfile-message: 4.0.3
vite-plus@0.1.22(@opentelemetry/api@1.9.1)(@types/node@25.9.1)(@voidzero-dev/vite-plus-core@0.1.22(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(typescript@6.0.3))(esbuild@0.28.0)(jiti@2.7.0)(jsdom@29.1.1(@noble/hashes@1.8.0))(typescript@6.0.3):
dependencies:
'@oxc-project/types': 0.129.0
'@oxlint/plugins': 1.61.0
'@voidzero-dev/vite-plus-core': 0.1.22(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(typescript@6.0.3)
'@voidzero-dev/vite-plus-test': 0.1.22(@opentelemetry/api@1.9.1)(@types/node@25.9.1)(@voidzero-dev/vite-plus-core@0.1.22(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(typescript@6.0.3))(esbuild@0.28.0)(jiti@2.7.0)(jsdom@29.1.1(@noble/hashes@1.8.0))(typescript@6.0.3)
oxfmt: 0.48.0
oxlint: 1.63.0(oxlint-tsgolint@0.22.1)
oxlint-tsgolint: 0.22.1
optionalDependencies:
'@voidzero-dev/vite-plus-darwin-arm64': 0.1.22
'@voidzero-dev/vite-plus-darwin-x64': 0.1.22
'@voidzero-dev/vite-plus-linux-arm64-gnu': 0.1.22
'@voidzero-dev/vite-plus-linux-arm64-musl': 0.1.22
'@voidzero-dev/vite-plus-linux-x64-gnu': 0.1.22
'@voidzero-dev/vite-plus-linux-x64-musl': 0.1.22
'@voidzero-dev/vite-plus-win32-arm64-msvc': 0.1.22
'@voidzero-dev/vite-plus-win32-x64-msvc': 0.1.22
transitivePeerDependencies:
- '@arethetypeswrong/core'
- '@edge-runtime/vm'
- '@opentelemetry/api'
- '@tsdown/css'
- '@tsdown/exe'
- '@types/node'
- '@vitejs/devtools'
- '@vitest/coverage-istanbul'
- '@vitest/coverage-v8'
- '@vitest/ui'
- bufferutil
- esbuild
- happy-dom
- jiti
- jsdom
- less
- publint
- sass
- sass-embedded
- stylus
- sugarss
- terser
- tsx
- typescript
- unplugin-unused
- unrun
- utf-8-validate
- vite
- yaml
vite-plus@0.1.22(@opentelemetry/api@1.9.1)(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(jsdom@29.1.1(@noble/hashes@1.8.0))(typescript@6.0.3)(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)): vite-plus@0.1.22(@opentelemetry/api@1.9.1)(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)(jsdom@29.1.1(@noble/hashes@1.8.0))(typescript@6.0.3)(vite@8.0.14(@types/node@25.9.1)(esbuild@0.28.0)(jiti@2.7.0)):
dependencies: dependencies:
'@oxc-project/types': 0.129.0 '@oxc-project/types': 0.129.0