fix: simplify file-preview sync, project-setup state, and remove unused deps

- Replace `file-preview`'s `useEffect`-based tree→URL sync with an `onSelectionChange` callback so the URL hash is the sole source of truth for `activePath`; drop `useFileTreeSelector` and the last-file latch ref in favor of a direct `hash && filePaths.includes(hash)` guard
- Simplify `ProjectSetup` to a nullable `draft` (null until first keystroke) + `useDebouncedCallback` so no sync effect or `lastSyncedRef` is needed to avoid echo-back from the URL-backed prop
- Remove `@tanstack/react-router-ssr-query`, `@tanstack/router-plugin`, `handlebars`, and `web-vitals` from `apps/web` deps — none were imported
- Delete the re-exported `summaryFor` helper from `adapter-switcher` (callers use `findModule` from `@stanza/registry` directly)
- Fix `PreviewBlock` code padding (`[&_pre]:p-4` → `[&_pre]:p-0`, add `mt-4` top margin)
- Fix intra-doc anchor in `getting-started.mdx` to use a relative `#` fragment instead of an absolute `/docs/getting-started#` path
This commit is contained in:
2026-05-26 14:00:02 -04:00
parent aa7c2bfe29
commit fc2cf8375d
19 changed files with 287 additions and 412 deletions
+1 -1
View File
@@ -25,7 +25,7 @@ npm create stanza@latest -- my-app
bun create stanza@latest my-app
```
To run it without the wizard — in CI, for example — see [Non-interactive setup](/docs/getting-started#non-interactive-setup) below.
To run it without the wizard — in CI, for example — see [Non-interactive setup](#non-interactive-setup) below.
## What you get
+4 -8
View File
@@ -26,20 +26,17 @@
"@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",
"@tanstack/react-start": "^1.168.13",
"@tanstack/router-plugin": "^1.168.11",
"@vercel/functions": "^3.6.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"fumadocs-core": "^16.9.1",
"fumadocs-mdx": "^15.0.8",
"fumadocs-mdx": "^15.0.9",
"fumadocs-ui": "npm:@fumadocs/base-ui@^16.9.1",
"handlebars": "^4.7.9",
"lru-cache": "^11.5.0",
"nitro": "^3.0.260522-beta",
"posthog-js": "^1.376.0",
"posthog-node": "^5.35.1",
"posthog-js": "^1.376.2",
"posthog-node": "^5.35.4",
"react": "^19.2.6",
"react-dom": "^19.2.6",
"react-resizable-panels": "^4.11.2",
@@ -66,7 +63,6 @@
"typescript": "^6.0.3",
"vite": "catalog:",
"vite-plus": "catalog:",
"vitest": "catalog:",
"web-vitals": "^5.2.0"
"vitest": "catalog:"
}
}
@@ -1,9 +1,9 @@
import { themeToTreeStyles } from "@pierre/trees";
import { FileTree, useFileTree, useFileTreeSelector } from "@pierre/trees/react";
import { FileTree, useFileTree } 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 } from "react";
import { useCallback, useEffect, useMemo, useRef } from "react";
import { useTheme } from "@/components/theme-provider";
import { Card } from "@/components/ui/card";
@@ -73,47 +73,64 @@ export function FilePreview({
/** Rendered into the pane's header bar (the install command row). */
header: ReactNode;
}) {
// URL hash mirrors the open file ("apps/web/src/index.tsx" etc.) so refreshes
// and shared links land on the same selection. Slashes are valid in a URL
// fragment, so paths go in raw — no encoding needed.
const hash = useLocation({ select: (l) => l.hash });
const navigate = useNavigate({ from: "/" });
const defaultPath = useMemo(() => defaultPathFor(filePaths), [filePaths]);
// The URL hash is the source of truth for "currently open file": folder
// clicks don't change it (handled in `onSelectionChange` below), so the
// preview pane keeps the last user-selected file even while a folder is
// visually selected in the tree.
const activePath = hash && filePaths.includes(hash) ? hash : defaultPath;
const preview = activePath ? previews[activePath] : undefined;
// `useFileTree` is lazy-init and reads its options once, so the selection
// handler bound at init must read latest navigate/hash/defaultPath via refs.
const navigateRef = useRef(navigate);
navigateRef.current = navigate;
const hashRef = useRef(hash);
hashRef.current = hash;
const defaultPathRef = useRef(defaultPath);
defaultPathRef.current = defaultPath;
const modelRef = useRef<ReturnType<typeof useFileTree>["model"] | null>(null);
// Tree → URL: fired by `@pierre/trees` on every selection change (user clicks
// OR programmatic `select()` from our reseed/hash-sync effects below). Skip
// folder selections so latching survives folder expansion, and skip no-op
// pushes so programmatic selects don't bounce back.
const onSelectionChange = useCallback((selectedPaths: readonly string[]) => {
const sel = selectedPaths[0];
if (!sel) return;
const item = modelRef.current?.getItem(sel);
if (!item || item.isDirectory()) return;
const nextHash = sel !== defaultPathRef.current ? sel : "";
if (nextHash === hashRef.current) return;
void navigateRef.current({
search: (prev) => prev,
hash: nextHash,
replace: true,
resetScroll: false,
hashScrollIntoView: false,
});
}, []);
// `useFileTree` builds the model once (lazy init) and doesn't react to later
// `paths` changes — we re-seed manually in the effect below.
const { model } = useFileTree({
paths: filePaths,
search: false,
unsafeCSS: TRUNCATE_FIX_CSS,
onSelectionChange,
});
// URL hash mirrors the open file ("apps/web/src/index.tsx" etc.) so refreshes
// and shared links land on the same selection. Slashes are valid in a URL
// fragment, so paths go in raw — no encoding needed.
const hash = useLocation({ select: (l) => l.hash });
// Read inside the reseed effect without making it a dep — otherwise every
// back/forward would collapse + re-expand the whole tree.
const hashRef = useRef(hash);
hashRef.current = hash;
const navigate = useNavigate({ from: "/" });
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
// changes, and `isDirectory()` is the canonical file/folder discriminator.
const fileSelection = useFileTreeSelector(model, (m) => {
const sel = m.getSelectedPaths()[0];
if (!sel) return undefined;
return m.getItem(sel)?.isDirectory() === false ? sel : undefined;
});
// Folder clicks in @pierre/trees both expand AND select, dropping the file
// selection. Latch the last file so folder expansion leaves the preview
// and the URL hash on the user's chosen file. Invalidate when the latched
// file disappears from filePaths (e.g. its owning module was deselected).
const lastFileRef = useRef<string | undefined>(undefined);
if (lastFileRef.current && !filePaths.includes(lastFileRef.current)) {
lastFileRef.current = undefined;
}
if (fileSelection !== undefined) lastFileRef.current = fileSelection;
const activePath = lastFileRef.current ?? defaultPath;
const preview = activePath ? previews[activePath] : undefined;
modelRef.current = model;
// Re-seed the model when `filePaths` changes. `resetPaths` collapses
// everything and clears selection, so we replay both manually.
// everything and clears selection, so we replay both manually. Tree → URL
// sync happens automatically via `onSelectionChange` when we call `select()`.
const prevPathsRef = useRef(filePaths);
useEffect(() => {
const expandedSet = new Set(
@@ -136,16 +153,10 @@ export function FilePreview({
}
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.
// Prefer the hash (deep link / refresh), else 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;
const next = initialHash && filePaths.includes(initialHash) ? initialHash : defaultPath;
// Reveal the selected file by expanding its ancestor chain — otherwise
// the tree selects a row that's collapsed out of view.
const expanded = [
@@ -157,8 +168,8 @@ export function FilePreview({
}, [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`.
// it in the tree. The resulting programmatic `select()` fires
// `onSelectionChange`, which short-circuits on the matching hash.
useEffect(() => {
if (!hash || !filePaths.includes(hash)) return;
if (model.getSelectedPaths()[0] === hash) return;
@@ -170,23 +181,6 @@ export function FilePreview({
model.getItem(hash)?.select();
}, [hash, filePaths, model]);
// Tree → URL: keep the hash in sync with the open file. The hash is empty
// when the active file is the implicit default (so shareable URLs stay
// clean) or when there's no valid file at all (so a stale hash gets
// cleared after the owning module is deselected).
useEffect(() => {
const nextHash =
activePath && filePaths.includes(activePath) && activePath !== defaultPath ? activePath : "";
if (nextHash === hash) return;
void navigate({
search: (prev) => prev,
hash: nextHash,
replace: true,
resetScroll: false,
hashScrollIntoView: false,
});
}, [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.
const { resolvedTheme } = useTheme();
+1 -1
View File
@@ -126,7 +126,7 @@ export function Builder({ state, search }: { state: BuilderState; search: Builde
onToggle={toggle}
/>
</section>
<section className="min-w-0 space-y-6 lg:sticky lg:top-20 lg:flex lg:h-[calc(100vh-6rem)] lg:flex-col lg:gap-6 lg:space-y-0 lg:self-start">
<section className="flex min-w-0 flex-col gap-6 lg:sticky lg:top-20 lg:h-[calc(100vh-6rem)] lg:self-start">
<div className="hidden lg:block">{commandBar}</div>
<FilePreview
filePaths={state.filePaths}
@@ -1,8 +1,8 @@
import { validateProjectName } from "@stanza/registry";
import { IconAlertCircle } from "@tabler/icons-react";
import { useDebouncedValue } from "@tanstack/react-pacer";
import { useDebouncedCallback } from "@tanstack/react-pacer";
import type { ChangeEvent } from "react";
import { useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
import { useId, useState } from "react";
import { Card } from "@/components/ui/card";
import { Field, FieldError } from "@/components/ui/field";
@@ -18,40 +18,34 @@ export function ProjectSetup({
defaultName: string;
onNameChange: (name: string) => void;
}) {
// `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);
// `draft` is null until the user types; the input then renders draft instead
// of the URL-backed prop so each keystroke is visible immediately while the
// debounced commit lags behind. We never echo the prop back into draft — no
// sync effect needed.
const [draft, setDraft] = useState<string | null>(null);
const display = draft ?? name;
const inputId = useId();
const errorId = `${inputId}-error`;
useEffect(() => {
if (name === lastSyncedRef.current) return;
lastSyncedRef.current = name;
setDraft(name);
}, [name]);
const validation = useMemo(() => validateProjectName(draft), [draft]);
const validation = validateProjectName(display);
// The placeholder telegraphs the `defaultName` fallback, so an empty field
// shouldn't render a "required" error.
const showError = !validation.ok && draft.trim().length > 0;
const showError = !validation.ok && display.trim().length > 0;
const [debouncedDraft] = useDebouncedValue(draft, { wait: 300 });
useEffect(() => {
if (debouncedDraft === lastSyncedRef.current) return;
if (!validateProjectName(debouncedDraft).ok) return;
lastSyncedRef.current = debouncedDraft;
onNameChange(debouncedDraft);
}, [debouncedDraft, onNameChange]);
const onDraftChange = useCallback(
(e: ChangeEvent<HTMLInputElement>) => setDraft(e.target.value),
[],
const commit = useDebouncedCallback(
(value: string) => {
if (validateProjectName(value).ok) onNameChange(value);
},
{ wait: 300 },
);
const onDraftChange = (e: ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
setDraft(value);
commit(value);
};
return (
<Card className="gap-0 px-3 py-3.5">
<Label htmlFor={inputId} className="mb-1.5 text-[13px] font-medium text-muted-foreground">
@@ -61,7 +55,7 @@ export function ProjectSetup({
<Input
id={inputId}
name="project-name"
value={draft}
value={display}
placeholder={defaultName}
onChange={onDraftChange}
autoComplete="off"
@@ -81,11 +81,3 @@ export function AdapterSwitcher({
</div>
);
}
export function summaryFor(
index: RegistryIndex,
slot: CategoryId,
id: string,
): ModuleSummary | undefined {
return index.modules.find((m) => m.category === slot && m.id === id);
}
@@ -64,7 +64,7 @@ function PreviewBlock({ preview }: { preview: Preview }) {
);
return (
<div
className="overflow-auto border-t border-border text-xs leading-relaxed [&_pre]:bg-transparent! [&_pre]:p-4!"
className="mt-4 overflow-auto border-t border-border text-xs leading-relaxed [&_pre]:bg-transparent! [&_pre]:p-0!"
dangerouslySetInnerHTML={inner}
/>
);
-2
View File
@@ -8,8 +8,6 @@ export function getMDXComponents(components?: MDXComponents) {
} satisfies MDXComponents;
}
export const useMDXComponents = getMDXComponents;
declare global {
type MDXProvidedComponents = ReturnType<typeof getMDXComponents>;
}
+53 -39
View File
@@ -4,11 +4,11 @@ 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 { useDebouncedCallback } from "@tanstack/react-pacer";
import { useNavigate } from "@tanstack/react-router";
import type { SortedResult } from "fumadocs-core/search";
import { useDocsSearch } from "fumadocs-core/search/client";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
import { ModuleLogo } from "@/components/module-logo";
import { Button } from "@/components/ui/button";
@@ -27,6 +27,13 @@ import type { DocsIndex } from "@/server/docs-index.functions";
const HOTKEY = "Mod+K";
const DEBOUNCE_MS = 150;
// Platform is fixed for the page's lifetime, so the subscribe callback is a
// no-op. Server snapshot hard-codes mac for SSR; the client snapshot reads
// navigator on hydration.
const subscribeNoop = () => () => {};
const getHotkeyLabel = () => formatForDisplay(HOTKEY);
const getServerHotkeyLabel = () => formatForDisplay(HOTKEY, { platform: "mac" });
/**
* One row's worth of docs result. Unified for empty-state pages (which carry
* a plain `description`) and searched hits (which carry `excerptHtml`, the
@@ -62,10 +69,7 @@ export function SiteSearch({ registry, docs }: { registry: RegistryIndex; docs:
const { isTouchDevice } = usePointerCapability();
useHotkey(HOTKEY, () => setOpen((o) => !o));
const [hotkeyLabel, setHotkeyLabel] = useState(() =>
formatForDisplay(HOTKEY, { platform: "mac" }),
);
useEffect(() => setHotkeyLabel(formatForDisplay(HOTKEY)), []);
const hotkeyLabel = useSyncExternalStore(subscribeNoop, getHotkeyLabel, getServerHotkeyLabel);
// Docs: Fumadocs' hook handles debounce + abort + caching internally.
const docsSearch = useDocsSearch({
@@ -73,33 +77,32 @@ export function SiteSearch({ registry, docs }: { registry: RegistryIndex; docs:
api: "/api/search/docs",
delayMs: DEBOUNCE_MS,
});
useEffect(() => {
docsSearch.setSearch(query);
}, [query, docsSearch]);
// 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 [debouncedQuery] = useDebouncedValue(query, { wait: DEBOUNCE_MS });
useEffect(() => {
if (!debouncedQuery.trim()) {
setModuleResults(null);
return () => {};
}
const controller = new AbortController();
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 moduleFetchRef = useRef<AbortController | null>(null);
const fetchModules = useDebouncedCallback(
(q: string) => {
moduleFetchRef.current?.abort();
if (!q.trim()) {
setModuleResults(null);
return;
}
const controller = new AbortController();
moduleFetchRef.current = controller;
fetch(`/api/search/modules?q=${encodeURIComponent(q)}`, { 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([]);
});
},
{ wait: DEBOUNCE_MS },
);
const pageTitlesByUrl = useMemo(
() => new Map(docs.pages.map((p) => [p.url, p.title])),
@@ -146,7 +149,12 @@ export function SiteSearch({ registry, docs }: { registry: RegistryIndex; docs:
const flat: Hit[] = useMemo(() => groups.flatMap((g) => g.hits), [groups]);
useEffect(() => setActiveIndex(0), [query]);
const onQueryChange = (value: string) => {
setQuery(value);
setActiveIndex(0);
docsSearch.setSearch(value);
fetchModules(value);
};
const select = useCallback(
(hit: Hit) => {
@@ -163,13 +171,19 @@ export function SiteSearch({ registry, docs }: { registry: RegistryIndex; docs:
[navigate],
);
const onOpenChange = useCallback((next: boolean) => {
setOpen(next);
if (next) {
setQuery("");
setActiveIndex(0);
}
}, []);
const onOpenChange = useCallback(
(next: boolean) => {
setOpen(next);
if (next) {
setQuery("");
setActiveIndex(0);
docsSearch.setSearch("");
moduleFetchRef.current?.abort();
setModuleResults(null);
}
},
[docsSearch],
);
const onKeyDown = useCallback(
(e: React.KeyboardEvent) => {
@@ -221,7 +235,7 @@ export function SiteSearch({ registry, docs }: { registry: RegistryIndex; docs:
autoFocus={!isTouchDevice}
type="search"
value={query}
onChange={(e) => setQuery(e.target.value)}
onChange={(e) => onQueryChange(e.target.value)}
onKeyDown={onKeyDown}
placeholder="Search docs and modules…"
aria-label="Search docs and modules"
@@ -333,7 +347,7 @@ function SearchRow({
data-selected={active || undefined}
onClick={handleSelect}
onPointerMove={handlePointerMove}
className="flex w-full cursor-pointer items-center gap-2 rounded-none px-2 py-2 text-left text-xs outline-hidden data-[selected]:bg-muted data-[selected]:text-foreground"
className="flex w-full cursor-pointer items-center gap-2 rounded-none p-2 text-left text-xs outline-hidden data-[selected]:bg-muted data-[selected]:text-foreground"
>
<RowContent hit={hit} />
</button>
+16 -25
View File
@@ -1,7 +1,6 @@
"use client";
import { cva, type VariantProps } from "class-variance-authority";
import { useMemo } from "react";
import { Label } from "@/components/ui/label";
import { Separator } from "@/components/ui/separator";
@@ -172,34 +171,26 @@ function FieldError({
}: React.ComponentProps<"div"> & {
errors?: Array<{ message?: string } | undefined>;
}) {
const content = useMemo(() => {
if (children) {
return children;
}
if (!errors?.length) {
return null;
}
if (!children && !errors?.length) return null;
let content: React.ReactNode = children;
if (!content && errors?.length) {
const uniqueErrors = [...new Map(errors.map((error) => [error?.message, error])).values()];
if (uniqueErrors?.length == 1) {
return uniqueErrors[0]?.message;
}
return (
<ul className="ml-4 flex list-disc flex-col gap-1">
{uniqueErrors.map(
(error, index) => error?.message && <li key={error.message ?? index}>{error.message}</li>,
)}
</ul>
);
}, [children, errors]);
if (!content) {
return null;
content =
uniqueErrors.length === 1 ? (
uniqueErrors[0]?.message
) : (
<ul className="ml-4 flex list-disc flex-col gap-1">
{uniqueErrors.map(
(error, index) =>
error?.message && <li key={error.message ?? index}>{error.message}</li>,
)}
</ul>
);
}
if (!content) return null;
return (
<div
role="alert"
+7 -4
View File
@@ -4,6 +4,7 @@ import { Toggle as TogglePrimitive } from "@base-ui/react/toggle";
import { ToggleGroup as ToggleGroupPrimitive } from "@base-ui/react/toggle-group";
import { type VariantProps } from "class-variance-authority";
import * as React from "react";
import { useMemo } from "react";
import { toggleVariants } from "@/components/ui/toggle";
import { cn } from "@/lib/utils";
@@ -33,6 +34,10 @@ function ToggleGroup({
spacing?: number;
orientation?: "horizontal" | "vertical";
}) {
const contextValue = useMemo(
() => ({ variant, size, spacing, orientation }),
[variant, size, spacing, orientation],
);
return (
<ToggleGroupPrimitive
data-slot="toggle-group"
@@ -49,9 +54,7 @@ function ToggleGroup({
)}
{...props}
>
<ToggleGroupContext.Provider value={{ variant, size, spacing, orientation }}>
{children}
</ToggleGroupContext.Provider>
<ToggleGroupContext.Provider value={contextValue}>{children}</ToggleGroupContext.Provider>
</ToggleGroupPrimitive>
);
}
@@ -63,7 +66,7 @@ function ToggleGroupItem({
size = "default",
...props
}: TogglePrimitive.Props & VariantProps<typeof toggleVariants>) {
const context = React.useContext(ToggleGroupContext);
const context = React.use(ToggleGroupContext);
return (
<TogglePrimitive
+1 -1
View File
@@ -92,7 +92,7 @@ function TooltipContent({
}
function useTooltipContext(componentName: string) {
const ctx = React.useContext(TooltipContext);
const ctx = React.use(TooltipContext);
if (!ctx) {
throw new Error(`${componentName} must be used within <Tooltip>.`);
+5 -67
View File
@@ -1,14 +1,5 @@
import type {
AppSpec,
CategoryId,
InstallHome,
Module,
Resolved,
ResolvedEntry,
TemplateRef,
} from "@stanza/registry";
import type { AppSpec, CategoryId, Module, Resolved, ResolvedEntry } from "@stanza/registry";
import {
categoryHome,
categoryOrder,
defaultWebApp,
emptyManifest,
@@ -46,10 +37,10 @@ export function parseSelections(search: BuilderSearch): {
for (const category of KNOWN_CATEGORIES) {
const value = search[category];
if (typeof value === "string" && value.length > 0) {
const ids = value
.split(",")
.map((s) => s.trim())
.filter(Boolean);
const ids = value.split(",").flatMap((s) => {
const trimmed = s.trim();
return trimmed ? [trimmed] : [];
});
if (ids.length > 0) selections[category] = ids;
}
}
@@ -160,65 +151,12 @@ export function pruneUnresolved(
}
}
export type SelectedFile = {
path: string;
template: TemplateRef;
owner: { category: CategoryId; module: string };
};
/**
* Default app list for the web builder. Single-app today (matches what
* `stanza init` produces); multi-app builder UX is a planned follow-up.
*/
export const DEFAULT_BUILDER_APPS: readonly AppSpec[] = [defaultWebApp()];
/**
* Derive the full file list stanza will write for the current selection.
* Mirrors codemod-runner's resolution via `categoryHome`:
* - repo → repo root · app → each app's dir · package → `packages/<dir>/`.
* Categories are emitted in `categoryOrder`. Each `scope: "app"` template
* emits once per app in `apps` (the builder defaults to a single web app).
*/
export function selectedFiles(
resolved: Resolved,
apps: readonly AppSpec[] = DEFAULT_BUILDER_APPS,
): SelectedFile[] {
const out: SelectedFile[] = [];
for (const category of categoryOrder) {
const home = categoryHome(category);
for (const entry of resolved[category] ?? []) {
for (const tpl of entry.adapter.templates ?? []) {
if (tpl.scope === "app") {
for (const app of apps) {
out.push({
path: resolveTemplatePath(tpl, home, app),
template: tpl,
owner: { category, module: entry.module.id },
});
}
continue;
}
out.push({
path: resolveTemplatePath(tpl, home, apps[0]!),
template: tpl,
owner: { category, module: entry.module.id },
});
}
}
}
return out;
}
function resolveTemplatePath(tpl: TemplateRef, home: InstallHome, app: AppSpec): string {
if (tpl.scope === "repo") return tpl.dest;
if (tpl.scope === "package") {
// Defensive: if a module declares `scope: "package"` for a non-package home,
// the CLI runner would error; the preview just falls back to repo root.
return home.kind === "package" ? `packages/${home.dir}/${tpl.dest}` : tpl.dest;
}
return `${app.dir.replace(/\/$/, "")}/${tpl.dest}`;
}
/**
* Build the `<pm> create stanza` command from the current state. npm needs a
* `--` separator to forward flags; pnpm/bun pass them through directly.
+44 -34
View File
@@ -33,46 +33,56 @@ async function buildIndex(): Promise<ModuleIndex> {
const db = create({ schema: moduleSchema });
const summaries = new Map<string, ModuleSummary>();
for (const summary of index.modules) {
let mod: Module | null = null;
try {
mod = await loadRegistryFile<Module>(`modules/${summary.category}-${summary.id}.json`);
} catch {
// Per-module file missing (shouldn't happen for first-party modules, but
// be tolerant): fall back to summary-only indexing.
}
const deps = new Set<string>();
const envNames = new Set<string>();
const envDescriptions: string[] = [];
if (mod) {
for (const k of Object.keys(mod.dependencies ?? {})) deps.add(k);
for (const k of Object.keys(mod.devDependencies ?? {})) deps.add(k);
for (const e of mod.env ?? []) {
envNames.add(e.name);
if (e.description) envDescriptions.push(e.description);
// Per-module file missing (shouldn't happen for first-party modules, but
// be tolerant): fall back to summary-only indexing via `mod: null`.
const loaded = await Promise.all(
index.modules.map(async (summary) => {
try {
const mod = await loadRegistryFile<Module>(
`modules/${summary.category}-${summary.id}.json`,
);
return { summary, mod };
} catch {
return { summary, mod: null as Module | null };
}
for (const adapter of mod.adapters) {
for (const k of Object.keys(adapter.dependencies ?? {})) deps.add(k);
for (const k of Object.keys(adapter.devDependencies ?? {})) deps.add(k);
for (const e of adapter.env ?? []) {
}),
);
const inserted = await Promise.all(
loaded.map(async ({ summary, mod }) => {
const deps = new Set<string>();
const envNames = new Set<string>();
const envDescriptions: string[] = [];
if (mod) {
for (const k of Object.keys(mod.dependencies ?? {})) deps.add(k);
for (const k of Object.keys(mod.devDependencies ?? {})) deps.add(k);
for (const e of mod.env ?? []) {
envNames.add(e.name);
if (e.description) envDescriptions.push(e.description);
}
for (const adapter of mod.adapters) {
for (const k of Object.keys(adapter.dependencies ?? {})) deps.add(k);
for (const k of Object.keys(adapter.devDependencies ?? {})) deps.add(k);
for (const e of adapter.env ?? []) {
envNames.add(e.name);
if (e.description) envDescriptions.push(e.description);
}
}
}
}
const id = await insert(db, {
category: summary.category,
moduleId: summary.id,
label: summary.label,
description: summary.description,
deps: [...deps].join(" "),
envNames: [...envNames].join(" "),
envDescriptions: envDescriptions.join(" "),
});
summaries.set(id, summary);
}
const id = await insert(db, {
category: summary.category,
moduleId: summary.id,
label: summary.label,
description: summary.description,
deps: [...deps].join(" "),
envNames: [...envNames].join(" "),
envDescriptions: envDescriptions.join(" "),
});
return { id, summary };
}),
);
for (const { id, summary } of inserted) summaries.set(id, summary);
return { db, summaries };
}
+6 -1
View File
@@ -2,6 +2,7 @@ import { createFileRoute, notFound } from "@tanstack/react-router";
import { createServerFn } from "@tanstack/react-start";
import { renderServerComponent } from "@tanstack/react-start/rsc";
import { DocsBody } from "fumadocs-ui/layouts/docs/page";
import { createRelativeLink } from "fumadocs-ui/mdx";
import { RootProvider } from "fumadocs-ui/provider/tanstack";
import { DocsSidebar } from "@/components/docs/docs-sidebar";
@@ -37,7 +38,11 @@ const getDocLayout = createServerFn({ method: "GET" })
</p>
)}
<DocsBody className="mt-8">
<MDX components={getMDXComponents()} />
<MDX
components={getMDXComponents({
a: createRelativeLink(source, page),
})}
/>
</DocsBody>
</article>
<DocsToc toc={page.data.toc} />
+1 -1
View File
@@ -203,7 +203,7 @@ function StatsPage() {
</CardContent>
</Card>
</div>
<p className="mt-4 text-xs leading-relaxed text-muted-foreground">
<p className="mt-4 text-xs leading-7 text-muted-foreground">
<strong className="font-medium text-foreground">Opt out</strong> per-invocation with{" "}
<code>--no-telemetry</code>, persistently with <code>STANZA_TELEMETRY=0</code> or{" "}
<code>DO_NOT_TRACK=1</code>. More in the{" "}
@@ -114,7 +114,11 @@ function computePeerOptions(
out[category] = constraint;
} else {
// "any" or undefined — list every module that lives in this category.
out[category] = index.modules.filter((m) => m.category === category).map((m) => m.id);
const ids: string[] = [];
for (const m of index.modules) {
if (m.category === category) ids.push(m.id);
}
out[category] = ids;
}
// De-dup and keep declaration order. Also union in any ids referenced by
// adapters that weren't in the declared list — defensive against authors
@@ -203,7 +207,7 @@ function pickAdapter(module: Module, peers: Partial<Record<CategoryId, string>>)
* - `dependencies` / `devDependencies` / `scripts` merge per-key
* - `env` merges by `name` (adapter overrides module)
*/
export function mergeInstallFields(module: Module, adapter: ModuleAdapter): EffectiveInstallFields {
function mergeInstallFields(module: Module, adapter: ModuleAdapter): EffectiveInstallFields {
const dependencies: Record<string, string> = {
...module.dependencies,
...adapter.dependencies,
-4
View File
@@ -29,9 +29,5 @@
"typescript": "^6.0.3",
"vite-plus": "catalog:"
},
"engines": {
"node": ">=22",
"pnpm": ">=10"
},
"packageManager": "pnpm@10.33.4"
}
+63 -123
View File
@@ -130,15 +130,9 @@ importers:
'@tanstack/react-router-devtools':
specifier: ^1.167.0
version: 1.167.0(@tanstack/react-router@1.170.8(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@tanstack/router-core@1.171.6)(csstype@3.2.3)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
'@tanstack/react-router-ssr-query':
specifier: ^1.167.0
version: 1.167.0(@tanstack/query-core@5.100.14)(@tanstack/react-query@5.100.14(react@19.2.6))(@tanstack/react-router@1.170.8(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@tanstack/router-core@1.171.6)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
'@tanstack/react-start':
specifier: ^1.168.13
version: 1.168.13(@vitejs/plugin-rsc@0.5.26(@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))(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@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))(crossws@0.4.5(srvx@0.11.16))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
'@tanstack/router-plugin':
specifier: ^1.168.11
version: 1.168.11(@tanstack/react-router@1.170.8(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@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))
'@vercel/functions':
specifier: ^3.6.0
version: 3.6.0
@@ -152,14 +146,11 @@ importers:
specifier: ^16.9.1
version: 16.9.1(@mdx-js/mdx@3.1.1)(@tanstack/react-router@1.170.8(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.15)(lucide-react@1.16.0(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(zod@4.4.3)
fumadocs-mdx:
specifier: ^15.0.8
version: 15.0.8(@types/mdast@4.0.4)(@types/mdx@2.0.13)(@types/react@19.2.15)(@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))(fumadocs-core@16.9.1(@mdx-js/mdx@3.1.1)(@tanstack/react-router@1.170.8(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.15)(lucide-react@1.16.0(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(zod@4.4.3))(react@19.2.6)(rolldown@1.0.2)
specifier: ^15.0.9
version: 15.0.9(@types/mdast@4.0.4)(@types/mdx@2.0.13)(@types/react@19.2.15)(@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))(fumadocs-core@16.9.1(@mdx-js/mdx@3.1.1)(@tanstack/react-router@1.170.8(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.15)(lucide-react@1.16.0(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(zod@4.4.3))(react@19.2.6)(rolldown@1.0.2)
fumadocs-ui:
specifier: npm:@fumadocs/base-ui@^16.9.1
version: '@fumadocs/base-ui@16.9.1(@tailwindcss/oxide@4.3.0)(@takumi-rs/image-response@1.5.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@types/mdx@2.0.13)(@types/react@19.2.15)(fumadocs-core@16.9.1(@mdx-js/mdx@3.1.1)(@tanstack/react-router@1.170.8(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.15)(lucide-react@1.16.0(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(zod@4.4.3))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(tailwindcss@4.3.0)'
handlebars:
specifier: ^4.7.9
version: 4.7.9
lru-cache:
specifier: ^11.5.0
version: 11.5.0
@@ -167,11 +158,11 @@ importers:
specifier: ^3.0.260522-beta
version: 3.0.260522-beta(@vercel/functions@3.6.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))(chokidar@5.0.0)(dotenv@17.4.2)(jiti@2.7.0)(lru-cache@11.5.0)
posthog-js:
specifier: ^1.376.0
version: 1.376.0
specifier: ^1.376.2
version: 1.376.2
posthog-node:
specifier: ^5.35.1
version: 5.35.1
specifier: ^5.35.4
version: 5.35.4
react:
specifier: ^19.2.6
version: 19.2.6
@@ -248,9 +239,6 @@ importers:
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)'
web-vitals:
specifier: ^5.2.0
version: 5.2.0
packages/codemods:
dependencies:
@@ -729,8 +717,8 @@ packages:
resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==}
engines: {node: '>=20.19.0'}
'@dotenvx/dotenvx@1.68.1':
resolution: {integrity: sha512-UQgds8NIGJEi/goJtAdPseaVhwz6xWVDik7P0Y1M35KKjTcjWZdsWWaswvDse6cxiBQel9Q7MQFMRxdMAQ2tqA==}
'@dotenvx/dotenvx@1.69.1':
resolution: {integrity: sha512-kwQB5KcAegxw/+NGUgXAo5ovyOSjlMhoXSSnSEpDhoHJwzMcMO0HE1U0VCYZ7jbAeCMGamed9XdWzOA5ixtTNg==}
hasBin: true
'@ecies/ciphers@0.2.6':
@@ -971,12 +959,12 @@ packages:
peerDependencies:
hono: ^4
'@inquirer/ansi@2.0.5':
resolution: {integrity: sha512-doc2sWgJpbFQ64UflSVd17ibMGDuxO1yKgOgLMwavzESnXjFWJqUeG8saYosqKpHp4kWiM5x1nXvEjbpx90gzw==}
'@inquirer/ansi@2.0.6':
resolution: {integrity: sha512-I/INw4sHGlVZ/afZOckpLiDP9SmbMl1g/GCqeHjLw1Afw/0PlRs2tRFgTGWmdI0hoNuWZn3y2iHNmG1vyECyQQ==}
engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'}
'@inquirer/confirm@6.0.13':
resolution: {integrity: sha512-wkGPC7yJ5WJk1DJ5SX7fzk+gfj4BM8cf5dDDi71B/551xHrdsZVRJOC0WyikXd0pEsb/9cLniuE4atbsMqmFkw==}
'@inquirer/confirm@6.1.0':
resolution: {integrity: sha512-USpeB76eqK7yGricDlGAupxWlp4a59qpeZOoNWaxO/nJln7agpJveyNkQ1d5u8YXG6TOqxZtQpKPORQQDrdVsA==}
engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'}
peerDependencies:
'@types/node': '>=18'
@@ -984,8 +972,8 @@ packages:
'@types/node':
optional: true
'@inquirer/core@11.1.10':
resolution: {integrity: sha512-a4Q5BXHQAHa9eO202sTaFCHFYVB3x5fauDuThEAdZ9gfn76pSxiKU7wWcEH0N1O0XmQvNfQNU6QXpiRxmYQx+A==}
'@inquirer/core@11.2.0':
resolution: {integrity: sha512-joR1YS2sI0us+9d0I8ViqFbrRLONO8CFTuyvBX4ZVBSch+VsZiugUABdrhBXXJR1VyEzvpz5SQCix3keETQ58g==}
engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'}
peerDependencies:
'@types/node': '>=18'
@@ -1002,12 +990,12 @@ packages:
'@types/node':
optional: true
'@inquirer/figures@2.0.5':
resolution: {integrity: sha512-NsSs4kzfm12lNetHwAn3GEuH317IzpwrMCbOuMIVytpjnJ90YYHNwdRgYGuKmVxwuIqSgqk3M5qqQt1cDk0tGQ==}
'@inquirer/figures@2.0.6':
resolution: {integrity: sha512-dsZgQtH2t5Q6ah3aPbZbeEZAxsD9qQu0DXf01AltuEfRTm+NoLN6+rLVbr+4edeEbNCp/wBNM6mALRWtsQpfkw==}
engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'}
'@inquirer/type@4.0.5':
resolution: {integrity: sha512-aetVUNeKNc/VriqXlw1NRSW0zhMBB0W4bNbWRJgzRl/3d0QNDQFfk0GO5SDdtjMZVg6o8ZKEiadd7SCCzoOn5Q==}
'@inquirer/type@4.0.6':
resolution: {integrity: sha512-J+9tdxOskuYuGjsvGaq00AamhDgjR7anhEW2dP4QdQpFCMPngCeC/bCYWQ5NsMWZRdsy53is7kAHb/+7cwDk2g==}
engines: {node: '>=23.5.0 || ^22.13.0 || ^21.7.0 || ^20.12.0'}
peerDependencies:
'@types/node': '>=18'
@@ -1615,11 +1603,11 @@ packages:
'@polka/url@1.0.0-next.29':
resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
'@posthog/core@1.29.9':
resolution: {integrity: sha512-DjvuIyBZ2Z/gBhtZlITlM2D8PlnMsHSQ1D78dbUYoVsgGguvanpJTobZObjLlFkybyvfZFYkpoJkFNI/2Pw4IQ==}
'@posthog/core@1.29.11':
resolution: {integrity: sha512-/4EF7oxAFSWJgaXxppT8bdYp7MGAnWFnKz994+MetTz/T6CKbYpjqIXHCofQXtcOXjEclTYj91igA+IkVFKiSg==}
'@posthog/types@1.376.0':
resolution: {integrity: sha512-gbFfxCuZDs/D4QZMwdE+smD1jsuqgGpS6yKGHZZ19foxMy8RYHsU1E47iG1b88n/uN02fAabLibVwuxLtq8juw==}
'@posthog/types@1.376.2':
resolution: {integrity: sha512-Y3ROpAxNqgcy2G0w6JoG5Gt+P6WNY2lkHTPMPzWqexRwemYbFegDi5AifDyD9/tstKTlOYKTTExtaJ5EBcghyQ==}
'@protobufjs/aspromise@1.1.2':
resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==}
@@ -2048,9 +2036,6 @@ packages:
resolution: {integrity: sha512-hB01dd4rlsYcTCNP7wK186jgAe6K5qimgM1Y5Jtvz+9PUaILvpmeLLjmQNUNSO1l23lIt+CeQR6mO1mjlPvRtQ==}
engines: {node: '>=18'}
'@tanstack/query-core@5.100.14':
resolution: {integrity: sha512-5X41dGpxgeaHISCRW2oYwcSycZeULZzAunaudXT9ov1KOTj9xwt0CH6hbwqP1/z74ZWF7rYFnDpyYH07XFcZew==}
'@tanstack/react-devtools@0.10.5':
resolution: {integrity: sha512-orVsRJ7oAXFb7oyafQCgx9YuK44jpILh5T/ddYuxAsolNfN5DZBr5/NLrWErD7HCGIzvYzg1TZI4sPxmiKvtvA==}
engines: {node: '>=18'}
@@ -2074,11 +2059,6 @@ packages:
react: '>=16.8'
react-dom: '>=16.8'
'@tanstack/react-query@5.100.14':
resolution: {integrity: sha512-oOr6aRdSFEwWhzxEkD/9ZcItM3+LjBSkeVmadWKwUssAHTsqd/7bOjWrX4AbvEkoEhgAxzN0Xk6H/aYzXiYBAw==}
peerDependencies:
react: ^18 || ^19
'@tanstack/react-router-devtools@1.167.0':
resolution: {integrity: sha512-nGw095EG7IHx0h5NtlEmzf6vcCTaFNPWdTSuDKazajhN0ct/v/TkekJ9J6KYUCeV1a8/2ZmToc58M+0rrOyn7w==}
engines: {node: '>=20.19'}
@@ -2091,16 +2071,6 @@ packages:
'@tanstack/router-core':
optional: true
'@tanstack/react-router-ssr-query@1.167.0':
resolution: {integrity: sha512-lJC/qySnlB0RaPCwCS4BQbdsDwyaPP2C8tzuEUsrwPTxm8TVYonYv3sptoSVhY0C2f8i5041X8gbRL7+lSY8BQ==}
engines: {node: '>=20.19'}
peerDependencies:
'@tanstack/query-core': '>=5.90.0'
'@tanstack/react-query': '>=5.90.0'
'@tanstack/react-router': '>=1.127.0'
react: '>=18.0.0 || >=19.0.0'
react-dom: '>=18.0.0 || >=19.0.0'
'@tanstack/react-router@1.170.8':
resolution: {integrity: sha512-Qw2ju6jjnIsMpuW+VrnHZWHuugqs592PWsnI56sG28qNhg14CgRLahOcNajfuJR9P4MxKGP94WVzmFKSYUz/ig==}
engines: {node: '>=20.19'}
@@ -2207,13 +2177,6 @@ packages:
webpack:
optional: true
'@tanstack/router-ssr-query-core@1.169.0':
resolution: {integrity: sha512-zueXiVsF1BbVc8iaalHILRGURDCVlTTOVdUy/36VVeKVKr778vqJzyus+erEoVu5x4vl4DBGGM8RHqCaus1TQQ==}
engines: {node: '>=20.19'}
peerDependencies:
'@tanstack/query-core': '>=5.90.0'
'@tanstack/router-core': '>=1.127.0'
'@tanstack/router-utils@1.162.1':
resolution: {integrity: sha512-62layyTGmclHDQS/eidwKRfN1hhCKwViG7iEBcVmL0MXgcAB3OOucWCEcDDGd9Cu11H6b4QQ5oOo47MWIqwz0A==}
engines: {node: '>=20.19'}
@@ -2857,8 +2820,8 @@ packages:
dataloader@1.4.0:
resolution: {integrity: sha512-68s5jYdlvasItOJnCuI2Q9s4q98g0pCyL3HrcKJu8KNugUl8ahgmZYg38ysLTgQjjXX3H8CJLkAvWrclWfcalw==}
dayjs@1.11.20:
resolution: {integrity: sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==}
dayjs@1.11.21:
resolution: {integrity: sha512-98IT+HOahAisibz/yjKbzuOBwYcjJ7BCLPzARyHiyEBmRz4fatF+KPJszEHXsGYjUG234aH/cOjW1wwTbKUZlA==}
db0@0.3.4:
resolution: {integrity: sha512-RiXXi4WaNzPTHEOu8UPQKMooIbqOEyqA1t7Z6MsdxSCeb8iUC9ko3LcmsLmeUt2SM5bctfArZKkRQggKZz7JNw==}
@@ -2965,8 +2928,8 @@ packages:
resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
engines: {node: '>= 4'}
dompurify@3.4.5:
resolution: {integrity: sha512-OrwIBKsdNSVEeubdJ1HBv/wNENRM9ytAVCv7YXt//A3vPdVMNuACRqK9mXCGCBW2ln7BT/A4X0jXHo2Gu89miA==}
dompurify@3.4.6:
resolution: {integrity: sha512-+7gzEI8trIIQkVCvQ3ucGtNfH3nOmDgVTzc62rAAOlMxLth78pwpPoZCPc7CyRzAQF89MqcfPdEWkDwnjgqktg==}
domutils@3.2.2:
resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==}
@@ -3312,8 +3275,8 @@ packages:
zod:
optional: true
fumadocs-mdx@15.0.8:
resolution: {integrity: sha512-CT9odtkLmpjH/DOTEW8prKY6SYzr7rmHmJj+ZBkvppPMNFstvTLvxxpY0ri8/9RPmCTZQUUMdNBxcPzhsHIcgw==}
fumadocs-mdx@15.0.9:
resolution: {integrity: sha512-ulKFGcx1r5Irk3Cu7h8M/DH66xba9evdOftURBbnASXWwJIIIahAp7s8oQdDuXtvkzjfv4/M/LpkTxrhkiS2JQ==}
hasBin: true
peerDependencies:
'@types/mdast': '*'
@@ -4079,9 +4042,9 @@ packages:
typescript:
optional: true
mute-stream@3.0.0:
resolution: {integrity: sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw==}
engines: {node: ^20.17.0 || >=22.9.0}
mute-stream@4.0.0:
resolution: {integrity: sha512-gSrprq0fJ3EiOErzjdIZrjysVVmJ4uu1QWfCDss5LypA5OXvrMje5Ym5z6V6RLyJ2eF87lasX7t6a0AnFvZblg==}
engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0}
nanoid@3.3.12:
resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==}
@@ -4365,11 +4328,11 @@ packages:
resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==}
engines: {node: ^10 || ^12 || >=14}
posthog-js@1.376.0:
resolution: {integrity: sha512-YGfQ6gSmqmEh287PHjXRDJ9zML3Su1UIt1+xjRy7Yk6yW43Sc7sFK3CpCkLchCGhIA4x6VaqK+LaqB+7+MCo7A==}
posthog-js@1.376.2:
resolution: {integrity: sha512-Anz2pCp7dcNbammTExiZpcKC08dxfrHYaJgaXH6rq5x3Zcfj/4FkcMJF2cGCrdQXel5Y4vftiVSseZda0HAQTQ==}
posthog-node@5.35.1:
resolution: {integrity: sha512-F9S3pEIYfGEVjLYIFHKaqfTIhn5IpS02Dkp7C/f1rqr4Z67Iqbt4jbKO8raWsT0veEI3rUp+DKuXLW1hN07FQA==}
posthog-node@5.35.4:
resolution: {integrity: sha512-X4OCh3Lr4tfyUc/67ssDhe5cD3fwFVq4QBdNpQwpjtuOCGWZ4wDONc6zSkE1i6FVUkTc884GvwphorKC6To/BQ==}
engines: {node: ^20.20.0 || >=22.22.0}
peerDependencies:
rxjs: ^7.0.0
@@ -5724,7 +5687,7 @@ snapshots:
'@csstools/css-tokenizer@4.0.0': {}
'@dotenvx/dotenvx@1.68.1':
'@dotenvx/dotenvx@1.69.1':
dependencies:
commander: 11.1.0
dotenv: 17.4.2
@@ -5898,23 +5861,23 @@ snapshots:
dependencies:
hono: 4.12.23
'@inquirer/ansi@2.0.5': {}
'@inquirer/ansi@2.0.6': {}
'@inquirer/confirm@6.0.13(@types/node@25.9.1)':
'@inquirer/confirm@6.1.0(@types/node@25.9.1)':
dependencies:
'@inquirer/core': 11.1.10(@types/node@25.9.1)
'@inquirer/type': 4.0.5(@types/node@25.9.1)
'@inquirer/core': 11.2.0(@types/node@25.9.1)
'@inquirer/type': 4.0.6(@types/node@25.9.1)
optionalDependencies:
'@types/node': 25.9.1
'@inquirer/core@11.1.10(@types/node@25.9.1)':
'@inquirer/core@11.2.0(@types/node@25.9.1)':
dependencies:
'@inquirer/ansi': 2.0.5
'@inquirer/figures': 2.0.5
'@inquirer/type': 4.0.5(@types/node@25.9.1)
'@inquirer/ansi': 2.0.6
'@inquirer/figures': 2.0.6
'@inquirer/type': 4.0.6(@types/node@25.9.1)
cli-width: 4.1.0
fast-wrap-ansi: 0.2.2
mute-stream: 3.0.0
mute-stream: 4.0.0
signal-exit: 4.1.0
optionalDependencies:
'@types/node': 25.9.1
@@ -5926,9 +5889,9 @@ snapshots:
optionalDependencies:
'@types/node': 25.9.1
'@inquirer/figures@2.0.5': {}
'@inquirer/figures@2.0.6': {}
'@inquirer/type@4.0.5(@types/node@25.9.1)':
'@inquirer/type@4.0.6(@types/node@25.9.1)':
optionalDependencies:
'@types/node': 25.9.1
@@ -6377,11 +6340,11 @@ snapshots:
'@polka/url@1.0.0-next.29': {}
'@posthog/core@1.29.9':
'@posthog/core@1.29.11':
dependencies:
'@posthog/types': 1.376.0
'@posthog/types': 1.376.2
'@posthog/types@1.376.0': {}
'@posthog/types@1.376.2': {}
'@protobufjs/aspromise@1.1.2': {}
@@ -6693,7 +6656,7 @@ snapshots:
'@tanstack/devtools-ui@0.5.2(csstype@3.2.3)(solid-js@1.9.13)':
dependencies:
clsx: 2.1.1
dayjs: 1.11.20
dayjs: 1.11.21
goober: 2.1.19(csstype@3.2.3)
solid-js: 1.9.13
transitivePeerDependencies:
@@ -6742,8 +6705,6 @@ snapshots:
'@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)':
dependencies:
'@tanstack/devtools': 0.12.2(csstype@3.2.3)(solid-js@1.9.13)
@@ -6771,11 +6732,6 @@ snapshots:
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
react: 19.2.6
'@tanstack/react-router-devtools@1.167.0(@tanstack/react-router@1.170.8(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@tanstack/router-core@1.171.6)(csstype@3.2.3)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)':
dependencies:
'@tanstack/react-router': 1.170.8(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
@@ -6787,17 +6743,6 @@ snapshots:
transitivePeerDependencies:
- csstype
'@tanstack/react-router-ssr-query@1.167.0(@tanstack/query-core@5.100.14)(@tanstack/react-query@5.100.14(react@19.2.6))(@tanstack/react-router@1.170.8(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@tanstack/router-core@1.171.6)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)':
dependencies:
'@tanstack/query-core': 5.100.14
'@tanstack/react-query': 5.100.14(react@19.2.6)
'@tanstack/react-router': 1.170.8(react-dom@19.2.6(react@19.2.6))(react@19.2.6)
'@tanstack/router-ssr-query-core': 1.169.0(@tanstack/query-core@5.100.14)(@tanstack/router-core@1.171.6)
react: 19.2.6
react-dom: 19.2.6(react@19.2.6)
transitivePeerDependencies:
- '@tanstack/router-core'
'@tanstack/react-router@1.170.8(react-dom@19.2.6(react@19.2.6))(react@19.2.6)':
dependencies:
'@tanstack/history': 1.162.0
@@ -6938,11 +6883,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@tanstack/router-ssr-query-core@1.169.0(@tanstack/query-core@5.100.14)(@tanstack/router-core@1.171.6)':
dependencies:
'@tanstack/query-core': 5.100.14
'@tanstack/router-core': 1.171.6
'@tanstack/router-utils@1.162.1':
dependencies:
'@babel/core': 7.29.7
@@ -7543,7 +7483,7 @@ snapshots:
dataloader@1.4.0: {}
dayjs@1.11.20: {}
dayjs@1.11.21: {}
db0@0.3.4: {}
@@ -7604,7 +7544,7 @@ snapshots:
dependencies:
domelementtype: 2.3.0
dompurify@3.4.5:
dompurify@3.4.6:
optionalDependencies:
'@types/trusted-types': 2.0.7
@@ -7983,7 +7923,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
fumadocs-mdx@15.0.8(@types/mdast@4.0.4)(@types/mdx@2.0.13)(@types/react@19.2.15)(@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))(fumadocs-core@16.9.1(@mdx-js/mdx@3.1.1)(@tanstack/react-router@1.170.8(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.15)(lucide-react@1.16.0(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(zod@4.4.3))(react@19.2.6)(rolldown@1.0.2):
fumadocs-mdx@15.0.9(@types/mdast@4.0.4)(@types/mdx@2.0.13)(@types/react@19.2.15)(@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))(fumadocs-core@16.9.1(@mdx-js/mdx@3.1.1)(@tanstack/react-router@1.170.8(react-dom@19.2.6(react@19.2.6))(react@19.2.6))(@types/estree-jsx@1.0.5)(@types/hast@3.0.4)(@types/mdast@4.0.4)(@types/react@19.2.15)(lucide-react@1.16.0(react@19.2.6))(react-dom@19.2.6(react@19.2.6))(react@19.2.6)(zod@4.4.3))(react@19.2.6)(rolldown@1.0.2):
dependencies:
'@mdx-js/mdx': 3.1.1
'@standard-schema/spec': 1.1.0
@@ -8983,7 +8923,7 @@ snapshots:
msw@2.14.6(@types/node@25.9.1)(typescript@6.0.3):
dependencies:
'@inquirer/confirm': 6.0.13(@types/node@25.9.1)
'@inquirer/confirm': 6.1.0(@types/node@25.9.1)
'@mswjs/interceptors': 0.41.9
'@open-draft/deferred-promise': 3.0.0
'@types/statuses': 2.0.6
@@ -9006,7 +8946,7 @@ snapshots:
transitivePeerDependencies:
- '@types/node'
mute-stream@3.0.0: {}
mute-stream@4.0.0: {}
nanoid@3.3.12: {}
@@ -9349,25 +9289,25 @@ snapshots:
picocolors: 1.1.1
source-map-js: 1.2.1
posthog-js@1.376.0:
posthog-js@1.376.2:
dependencies:
'@opentelemetry/api': 1.9.1
'@opentelemetry/api-logs': 0.208.0
'@opentelemetry/exporter-logs-otlp-http': 0.208.0(@opentelemetry/api@1.9.1)
'@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.1)
'@opentelemetry/sdk-logs': 0.208.0(@opentelemetry/api@1.9.1)
'@posthog/core': 1.29.9
'@posthog/types': 1.376.0
'@posthog/core': 1.29.11
'@posthog/types': 1.376.2
core-js: 3.49.0
dompurify: 3.4.5
dompurify: 3.4.6
fflate: 0.4.8
preact: 10.29.2
query-selector-shadow-dom: 1.0.1
web-vitals: 5.2.0
posthog-node@5.35.1:
posthog-node@5.35.4:
dependencies:
'@posthog/core': 1.29.9
'@posthog/core': 1.29.11
powershell-utils@0.1.0: {}
@@ -9719,7 +9659,7 @@ snapshots:
'@babel/parser': 7.29.7
'@babel/plugin-transform-typescript': 7.29.7(@babel/core@7.29.7)
'@babel/preset-typescript': 7.29.7(@babel/core@7.29.7)
'@dotenvx/dotenvx': 1.68.1
'@dotenvx/dotenvx': 1.69.1
'@modelcontextprotocol/sdk': 1.29.0(zod@3.25.76)
'@types/validate-npm-package-name': 4.0.2
browserslist: 4.28.2