mirror of
https://github.com/prdlk/dotfiles.git
synced 2026-11-07 20:47:37 +00:00
shell: fish functions -> ~/.local/bin executables + bash/zsh ports; manage ~/.agents
- 16 cross-shell utilities converted to sh/bash chezmoi executables: ai c2p cp-appid extract_agents fab gdc gh-sonr-org-migrate ghg-clone ghn gho git-summary launch_omp_livediff_overlay lc-elir mdcp mdp pj (ghg-clone not named ghg: unrelated cargo binary owns that name) - cwd/history helpers stay per-shell: yy c cd clip-last + ghg/ghn/gho cd-wrappers in bash/functions/ and zsh/functions/, loaded by rc files - fish keeps only shell-state functions; converted ones now hit PATH - cp-appid ignore gating moved to .local/bin path - dot_agents/skills: 82 skills materialized from dotstate storage so dot_claude skill symlinks resolve on fresh bootstrap; dotstate profile/symlinks.json released .agents/skills .bashrc .zshrc
This commit is contained in:
1 parent
6776366ce6
commit
6406208b35
637 files changed
+100297
-497
No files matched your search
@@ -0,0 +1,158 @@
|
||||
# search-validation: Always Validate Search Params
|
||||
|
||||
## Priority: HIGH
|
||||
|
||||
## Explanation
|
||||
|
||||
Search params come from the URL - user-controlled input that must be validated. Use `validateSearch` to parse, validate, and provide defaults. This ensures type safety and prevents runtime errors from malformed URLs.
|
||||
|
||||
## Bad Example
|
||||
|
||||
```tsx
|
||||
// No validation - trusting URL input directly
|
||||
export const Route = createFileRoute('/products')({
|
||||
component: ProductsPage,
|
||||
})
|
||||
|
||||
function ProductsPage() {
|
||||
// Accessing raw search params without validation
|
||||
const searchParams = new URLSearchParams(window.location.search)
|
||||
const page = parseInt(searchParams.get('page') || '1') // Could be NaN
|
||||
const sort = searchParams.get('sort') as 'asc' | 'desc' // Could be anything
|
||||
|
||||
// Runtime errors possible if URL is malformed
|
||||
return <ProductList page={page} sort={sort} />
|
||||
}
|
||||
```
|
||||
|
||||
## Good Example: Manual Validation
|
||||
|
||||
```tsx
|
||||
export const Route = createFileRoute('/products')({
|
||||
validateSearch: (search: Record<string, unknown>) => {
|
||||
return {
|
||||
page: Number(search.page) || 1,
|
||||
sort: search.sort === 'desc' ? 'desc' : 'asc',
|
||||
category: typeof search.category === 'string' ? search.category : undefined,
|
||||
minPrice: Number(search.minPrice) || undefined,
|
||||
maxPrice: Number(search.maxPrice) || undefined,
|
||||
}
|
||||
},
|
||||
component: ProductsPage,
|
||||
})
|
||||
|
||||
function ProductsPage() {
|
||||
// Fully typed, validated search params
|
||||
const { page, sort, category, minPrice, maxPrice } = Route.useSearch()
|
||||
// page: number (default 1)
|
||||
// sort: 'asc' | 'desc' (default 'asc')
|
||||
// category: string | undefined
|
||||
}
|
||||
```
|
||||
|
||||
## Good Example: With Zod
|
||||
|
||||
```tsx
|
||||
import { z } from 'zod'
|
||||
|
||||
const productSearchSchema = z.object({
|
||||
page: z.number().min(1).catch(1),
|
||||
limit: z.number().min(1).max(100).catch(20),
|
||||
sort: z.enum(['name', 'price', 'date']).catch('name'),
|
||||
order: z.enum(['asc', 'desc']).catch('asc'),
|
||||
category: z.string().optional(),
|
||||
search: z.string().optional(),
|
||||
minPrice: z.number().min(0).optional(),
|
||||
maxPrice: z.number().min(0).optional(),
|
||||
})
|
||||
|
||||
type ProductSearch = z.infer<typeof productSearchSchema>
|
||||
|
||||
export const Route = createFileRoute('/products')({
|
||||
validateSearch: (search) => productSearchSchema.parse(search),
|
||||
component: ProductsPage,
|
||||
})
|
||||
|
||||
function ProductsPage() {
|
||||
const search = Route.useSearch()
|
||||
// search: ProductSearch - fully typed with defaults
|
||||
|
||||
return (
|
||||
<ProductList
|
||||
page={search.page}
|
||||
limit={search.limit}
|
||||
sort={search.sort}
|
||||
order={search.order}
|
||||
filters={{
|
||||
category: search.category,
|
||||
search: search.search,
|
||||
priceRange: search.minPrice && search.maxPrice
|
||||
? [search.minPrice, search.maxPrice]
|
||||
: undefined,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Good Example: With Valibot
|
||||
|
||||
```tsx
|
||||
import * as v from 'valibot'
|
||||
import { valibotSearchValidator } from '@tanstack/router-valibot-adapter'
|
||||
|
||||
const searchSchema = v.object({
|
||||
page: v.fallback(v.number(), 1),
|
||||
query: v.fallback(v.string(), ''),
|
||||
filters: v.fallback(
|
||||
v.array(v.string()),
|
||||
[]
|
||||
),
|
||||
})
|
||||
|
||||
export const Route = createFileRoute('/search')({
|
||||
validateSearch: valibotSearchValidator(searchSchema),
|
||||
component: SearchPage,
|
||||
})
|
||||
```
|
||||
|
||||
## Updating Search Params
|
||||
|
||||
```tsx
|
||||
function ProductFilters() {
|
||||
const navigate = useNavigate()
|
||||
const search = Route.useSearch()
|
||||
|
||||
const updateFilters = (newFilters: Partial<ProductSearch>) => {
|
||||
navigate({
|
||||
to: '.', // Current route
|
||||
search: (prev) => ({
|
||||
...prev,
|
||||
...newFilters,
|
||||
page: 1, // Reset to page 1 when filters change
|
||||
}),
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<select
|
||||
value={search.sort}
|
||||
onChange={(e) => updateFilters({ sort: e.target.value as ProductSearch['sort'] })}
|
||||
>
|
||||
<option value="name">Name</option>
|
||||
<option value="price">Price</option>
|
||||
<option value="date">Date</option>
|
||||
</select>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
## Context
|
||||
|
||||
- Search params are user input - never trust them unvalidated
|
||||
- Use `.catch()` in Zod or `fallback()` in Valibot for graceful defaults
|
||||
- Validation runs on every navigation - keep it fast
|
||||
- Search params are inherited by child routes
|
||||
- Use `search` updater function to preserve other params
|
||||
Reference in new issue
Block a user