diff --git a/apps/web/package.json b/apps/web/package.json index 11a2d2e..3f896f7 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -23,6 +23,7 @@ "@takumi-rs/image-response": "^1.5.1", "@tanstack/react-devtools": "^0.10.5", "@tanstack/react-hotkeys": "^0.10.0", + "@tanstack/react-pacer": "^0.22.1", "@tanstack/react-router": "^1.170.8", "@tanstack/react-router-devtools": "^1.167.0", "@tanstack/react-router-ssr-query": "^1.167.0", diff --git a/apps/web/src/components/builder/file-preview.tsx b/apps/web/src/components/builder/file-preview.tsx index 6a61656..84989a5 100644 --- a/apps/web/src/components/builder/file-preview.tsx +++ b/apps/web/src/components/builder/file-preview.tsx @@ -3,11 +3,12 @@ import { FileTree, useFileTree, useFileTreeSelector } from "@pierre/trees/react" import { IconLoader2 } from "@tabler/icons-react"; import { useLocation, useNavigate } from "@tanstack/react-router"; 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 { Card } from "@/components/ui/card"; import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/components/ui/resizable"; +import { useGracePeriod } from "@/hooks/use-grace-period"; import { useMediaQuery } from "@/hooks/use-media-query"; import type { Preview } from "@/server/highlighter"; @@ -33,17 +34,17 @@ const TRUNCATE_FIX_CSS = ` 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 // 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). const OVERLAY_PENDING_MS = 300; 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"]. function directoryPaths(paths: readonly string[]): string[] { const dirs = new Set(); @@ -59,30 +60,6 @@ function directoryPaths(paths: readonly string[]): string[] { 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({ filePaths, previews, @@ -113,46 +90,7 @@ export function FilePreview({ const hashRef = useRef(hash); hashRef.current = hash; const navigate = useNavigate({ from: "/" }); - - // 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]); + const defaultPath = useMemo(() => defaultPathFor(filePaths), [filePaths]); // Project to "current file selection (undefined for nothing/folder)". The // selector pattern lets the tree skip re-rendering us on unrelated state @@ -171,15 +109,64 @@ export function FilePreview({ lastFileRef.current = undefined; } if (fileSelection !== undefined) lastFileRef.current = fileSelection; - const activePath = lastFileRef.current ?? defaultPathFor(filePaths); + const activePath = lastFileRef.current ?? defaultPath; 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 // it in the tree. Skipped on the initial reseed since that effect already // honors the hash via `hashRef`. useEffect(() => { if (!hash || !filePaths.includes(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(); }, [hash, filePaths, model]); @@ -189,9 +176,7 @@ export function FilePreview({ // cleared after the owning module is deselected). useEffect(() => { const nextHash = - activePath && filePaths.includes(activePath) && activePath !== defaultPathFor(filePaths) - ? activePath - : ""; + activePath && filePaths.includes(activePath) && activePath !== defaultPath ? activePath : ""; if (nextHash === hash) return; void navigate({ search: (prev) => prev, @@ -200,7 +185,7 @@ export function FilePreview({ resetScroll: 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 // `prefers-color-scheme` and mismatches when the user overrides the OS theme. diff --git a/apps/web/src/components/builder/project-setup.tsx b/apps/web/src/components/builder/project-setup.tsx index 2b64bc2..e1c08e4 100644 --- a/apps/web/src/components/builder/project-setup.tsx +++ b/apps/web/src/components/builder/project-setup.tsx @@ -1,7 +1,8 @@ import { validateProjectName } from "@stanza/registry"; import { IconAlertCircle } from "@tabler/icons-react"; +import { useDebouncedValue } from "@tanstack/react-pacer"; 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 { Field, FieldError } from "@/components/ui/field"; @@ -17,10 +18,9 @@ export function ProjectSetup({ defaultName: string; onNameChange: (name: string) => void; }) { - // Debounce the upward push (navigates → reruns the loader) while keeping the - // field responsive. `lastSyncedRef` ignores the echo of our own pushes — - // otherwise `setDraft(name)` would clobber characters the user typed between - // the debounce firing and the new `name` prop arriving. + // `lastSyncedRef` ignores the echo of our own pushes — otherwise + // `setDraft(name)` would clobber characters the user typed between the + // debounce firing and the new `name` prop arriving. const [draft, setDraft] = useState(name); const lastSyncedRef = useRef(name); @@ -38,19 +38,14 @@ export function ProjectSetup({ // shouldn't render a "required" error. const showError = !validation.ok && draft.trim().length > 0; - // Read the latest `onNameChange` without making the debounce effect re-run - // every time the parent rebuilds the callback. - const commit = useEffectEvent((next: string) => { - lastSyncedRef.current = next; - onNameChange(next); - }); + const [debouncedDraft] = useDebouncedValue(draft, { wait: 300 }); useEffect(() => { - if (draft === lastSyncedRef.current) return undefined; - if (!validation.ok) return undefined; - const timer = setTimeout(() => commit(draft), 300); - return () => clearTimeout(timer); - }, [draft, validation.ok]); + if (debouncedDraft === lastSyncedRef.current) return; + if (!validateProjectName(debouncedDraft).ok) return; + lastSyncedRef.current = debouncedDraft; + onNameChange(debouncedDraft); + }, [debouncedDraft, onNameChange]); const onDraftChange = useCallback( (e: ChangeEvent) => setDraft(e.target.value), diff --git a/apps/web/src/components/search/site-search.tsx b/apps/web/src/components/search/site-search.tsx index 01f8efa..a1b9dfa 100644 --- a/apps/web/src/components/search/site-search.tsx +++ b/apps/web/src/components/search/site-search.tsx @@ -4,6 +4,7 @@ import type { ModuleSummary, RegistryIndex } from "@stanza/registry"; import { categoryLabel } from "@stanza/registry"; import { IconBookmark, IconSearch } from "@tabler/icons-react"; import { formatForDisplay, useHotkey } from "@tanstack/react-hotkeys"; +import { useDebouncedValue } from "@tanstack/react-pacer"; import { useNavigate } from "@tanstack/react-router"; import type { SortedResult } from "fumadocs-core/search"; import { useDocsSearch } from "fumadocs-core/search/client"; @@ -76,30 +77,29 @@ export function SiteSearch({ registry, docs }: { registry: RegistryIndex; docs: docsSearch.setSearch(query); }, [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(null); + const [debouncedQuery] = useDebouncedValue(query, { wait: DEBOUNCE_MS }); useEffect(() => { - if (!query.trim()) { + if (!debouncedQuery.trim()) { setModuleResults(null); return () => {}; } const controller = new AbortController(); - const timer = setTimeout(() => { - fetch(`/api/search/modules?q=${encodeURIComponent(query)}`, { signal: controller.signal }) - .then((res) => res.json()) - .then((data: unknown) => setModuleResults(parseModuleResults(data))) - .catch((err: unknown) => { - if (err instanceof DOMException && err.name === "AbortError") return; - // Surface network/parse failures as "no matches" rather than blanking - // the dialog or throwing past React's render loop. - setModuleResults([]); - }); - }, DEBOUNCE_MS); - return () => { - clearTimeout(timer); - controller.abort(); - }; - }, [query]); + fetch(`/api/search/modules?q=${encodeURIComponent(debouncedQuery)}`, { + signal: controller.signal, + }) + .then((res) => res.json()) + .then((data: unknown) => setModuleResults(parseModuleResults(data))) + .catch((err: unknown) => { + if (err instanceof DOMException && err.name === "AbortError") return; + // Surface network/parse failures as "no matches" rather than blanking + // the dialog or throwing past React's render loop. + setModuleResults([]); + }); + return () => controller.abort(); + }, [debouncedQuery]); const pageTitlesByUrl = useMemo( () => new Map(docs.pages.map((p) => [p.url, p.title])), diff --git a/apps/web/src/hooks/use-grace-period.ts b/apps/web/src/hooks/use-grace-period.ts new file mode 100644 index 0000000..a6f86b6 --- /dev/null +++ b/apps/web/src/hooks/use-grace-period.ts @@ -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; +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3ae8197..b60a65b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -121,6 +121,9 @@ importers: '@tanstack/react-hotkeys': specifier: ^0.10.0 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': specifier: ^1.170.8 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)' vite-plus: 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: 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: specifier: ^5.2.0 version: 5.2.0 @@ -2014,6 +2017,10 @@ packages: resolution: {integrity: sha512-vqH7X9nb0MTJ/O08++dB5bP9jgj4+BIPOUu/U+6myG86lDsirZSVSobpq5UQpE7nBuk62i8eIYeOhd+OMl/UrA==} engines: {node: '>=18'} + '@tanstack/pacer@0.21.1': + resolution: {integrity: sha512-hB01dd4rlsYcTCNP7wK186jgAe6K5qimgM1Y5Jtvz+9PUaILvpmeLLjmQNUNSO1l23lIt+CeQR6mO1mjlPvRtQ==} + engines: {node: '>=18'} + '@tanstack/query-core@5.100.14': resolution: {integrity: sha512-5X41dGpxgeaHISCRW2oYwcSycZeULZzAunaudXT9ov1KOTj9xwt0CH6hbwqP1/z74ZWF7rYFnDpyYH07XFcZew==} @@ -2033,6 +2040,13 @@ packages: react: '>=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': resolution: {integrity: sha512-oOr6aRdSFEwWhzxEkD/9ZcItM3+LjBSkeVmadWKwUssAHTsqd/7bOjWrX4AbvEkoEhgAxzN0Xk6H/aYzXiYBAw==} peerDependencies: @@ -6646,6 +6660,11 @@ snapshots: dependencies: '@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/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-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)': dependencies: '@tanstack/query-core': 5.100.14 @@ -7076,48 +7102,6 @@ snapshots: '@voidzero-dev/vite-plus-linux-x64-musl@0.1.22': 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))': dependencies: '@standard-schema/spec': 1.1.0 @@ -9973,55 +9957,6 @@ snapshots: '@types/unist': 3.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)): dependencies: '@oxc-project/types': 0.129.0