FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

chore: add audit guardrails · TanStack/tanstack.com@0f55472 · GitHub

Commit 0f55472

Browse files
committed
chore: add audit guardrails
1 parent 3c60644 commit 0f55472

19 files changed

Lines changed: 302 additions & 42 deletions

‎docs/source-code-audit-2026-06-23.md‎

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@ Tracking model: work this list top to bottom. Leave untouched bullets plain, pre
1414

1515
These are quick, high-leverage patches because later fixes can reuse them instead of adding another local workaround.
1616

17-
- Make `pnpm test` honest about TypeScript tests, or split script names so validation expectations are clear.
18-
- Default non-submit buttons/select triggers to `type="button"`, fix the shared pagination label/id contract, and clean up tooltip child prop merging.
19-
- Add tiny helpers for response filenames, external URL normalization, internal route classification, package slug encoding, row-local ids, and guarded storage.
17+
- [done 2026-06-24: `pnpm run test:unit`, `pnpm test`] Make `pnpm test` honest about TypeScript tests, or split script names so validation expectations are clear.
18+
- [done 2026-06-24: `pnpm run test:tsc`, `pnpm run test:lint`] Default non-submit buttons/select triggers to `type="button"`, fix the shared pagination label/id contract, and clean up tooltip child prop merging.
19+
- [done 2026-06-24: `pnpm run test:unit`, `pnpm run test:tsc`, `pnpm run test:lint`] Add tiny helpers for response filenames, package slug encoding, and row-local ids.
20+
- Add tiny helpers for external URL normalization, internal route classification, and guarded storage.
2021
- Fix custom `useMutation` stale callbacks, clipboard/copied-state timers, docs sidebar timers, popup blocked-state handling, and simple page-visibility/reduced-motion helpers.
2122
- Normalize easy static data contracts: maintainer bare-domain URLs, Scarf id validation, public-library selector, partner analytics seed buckets, blog image URLs, host cache purge response policy.
2223

@@ -88,7 +89,10 @@ Treat these as tracked initiatives, not normal cleanup PRs.
8889

8990
## Batch Log
9091

91-
- 2026-06-23: Audit created and ordered. No implementation batch has landed yet.
92+
- 2026-06-24: Added shared response filename/content-disposition helpers, package route slug encoding helpers, and row-local id helpers; wired them into builder/docs downloads, Intent registry links, and moderation note inputs.
93+
- 2026-06-24: Hardened shared UI primitives: defaulted shop buttons and shared select triggers to non-submit buttons, gave pagination a unique page-size label/id pair plus non-submit controls, and made Tooltip merge trigger handlers/refs without `any`.
94+
- 2026-06-24: Added `test:unit` and wired it into `pnpm test` so existing TypeScript assertion tests run in the default validation path.
95+
- 2026-06-23: Audit created and ordered.
9296

9397
## Highest Priority Findings
9498

‎package.json‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,10 @@
2828
"docs:webhooks:sync": "tsx scripts/sync-docs-webhooks.ts",
2929
"husky": "pnpm run format && pnpm run test",
3030
"pretest": "pnpm run content:build",
31-
"test": "run-p test:tsc test:lint",
31+
"test": "run-p test:tsc test:lint test:unit",
3232
"test:tsc": "tsc",
3333
"test:lint": "pnpm run lint",
34+
"test:unit": "tsx --test \"tests/**/*.test.ts\"",
3435
"prepare": "husky"
3536
},
3637
"dependencies": {

‎src/components/FeedbackModerationList.tsx‎

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { PaginationControls } from './PaginationControls'
1414
import { Spinner } from './Spinner'
1515
import type { DocFeedback } from '~/db/types'
1616
import { calculatePoints } from '~/utils/docFeedback.shared'
17+
import { getRowFieldId } from '~/utils/route-encoding'
1718
import { Check, Lightbulb, TriangleAlert } from 'lucide-react'
1819
import { MessageSquare, X } from 'lucide-react'
1920
import { Badge, Button } from '~/ui'
@@ -155,6 +156,11 @@ export function FeedbackModerationList({
155156
const isExpanded = expandedIds.has(feedback.id)
156157
const isPending = feedback.status === 'pending'
157158
const isModeratingThis = isModeratingId === feedback.id
159+
const moderationNoteId = getRowFieldId(
160+
'feedback-moderation',
161+
feedback.id,
162+
'note',
163+
)
158164

159165
return (
160166
<React.Fragment key={feedback.id}>
@@ -333,12 +339,13 @@ export function FeedbackModerationList({
333339
{isPending && (
334340
<div>
335341
<label
336-
htmlFor={`moderation-note-${feedback.id}`}
342+
htmlFor={moderationNoteId}
337343
className="block text-sm font-semibold mb-2"
338344
>
339345
Internal Moderation Note (optional):
340346
</label>
341347
<textarea
348+
id={moderationNoteId}
342349
value={moderationNotes[feedback.id] || ''}
343350
onChange={(e) =>
344351
handleModerationNoteChange(

‎src/components/PaginationControls.tsx‎

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import * as React from 'react'
12
import { ChevronLeft, ChevronRight } from 'lucide-react'
23

34
interface PaginationControlsProps {
@@ -31,6 +32,8 @@ export function PaginationControls({
3132
itemLabel = 'items',
3233
sticky = false,
3334
}: PaginationControlsProps) {
35+
const pageSizeSelectId = `${React.useId()}-page-size`
36+
3437
const goToPreviousPage = () => {
3538
if (canGoPrevious) {
3639
onPageChange(currentPage - 1)
@@ -83,7 +86,10 @@ export function PaginationControls({
8386
}
8487

8588
const content = (
86-
<div className="flex items-center justify-between flex-wrap gap-3">
89+
<nav
90+
aria-label={`${itemLabel} pagination`}
91+
className="flex items-center justify-between flex-wrap gap-3"
92+
>
8793
<div className="text-sm text-gray-600 dark:text-gray-400">
8894
Showing{' '}
8995
<span className="font-semibold text-gray-900 dark:text-white">
@@ -109,12 +115,13 @@ export function PaginationControls({
109115
{showPageSizeSelector && (
110116
<>
111117
<label
112-
htmlFor="pageSize"
118+
htmlFor={pageSizeSelectId}
113119
className="text-sm text-gray-600 dark:text-gray-400"
114120
>
115121
Per page:
116122
</label>
117123
<select
124+
id={pageSizeSelectId}
118125
value={pageSize}
119126
onChange={(e) => {
120127
const next = parseInt(e.target.value, 10)
@@ -133,6 +140,7 @@ export function PaginationControls({
133140

134141
<div className="flex gap-1 items-center">
135142
<button
143+
type="button"
136144
onClick={goToPreviousPage}
137145
disabled={!canGoPrevious}
138146
className="flex items-center px-2 py-1 text-xs font-medium text-gray-700 bg-white border border-gray-300 rounded hover:bg-gray-50 hover:text-gray-900 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-700 dark:hover:text-white disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-white dark:disabled:hover:bg-gray-800 gap-1 transition-colors"
@@ -158,6 +166,7 @@ export function PaginationControls({
158166
const isActive = pageNum === currentPage
159167
return (
160168
<button
169+
type="button"
161170
key={pageNum}
162171
onClick={() => onPageChange(pageNum as number)}
163172
className={
@@ -173,6 +182,7 @@ export function PaginationControls({
173182
</div>
174183

175184
<button
185+
type="button"
176186
onClick={goToNextPage}
177187
disabled={!canGoNext}
178188
className="flex items-center px-2 py-1 text-xs font-medium text-gray-700 bg-white border border-gray-300 rounded hover:bg-gray-50 hover:text-gray-900 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:hover:bg-gray-700 dark:hover:text-white disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:bg-white dark:disabled:hover:bg-gray-800 gap-1 transition-colors"
@@ -182,7 +192,7 @@ export function PaginationControls({
182192
</button>
183193
</div>
184194
</div>
185-
</div>
195+
</nav>
186196
)
187197

188198
if (sticky) {

‎src/components/Select.tsx‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,10 @@ export function Select<T extends SelectOption>({
4343
<div className={twMerge('w-full', className)}>
4444
<Dropdown>
4545
<DropdownTrigger>
46-
<button className="relative items-center w-full gap-2 flex hover:bg-gray-500/10 cursor-pointer rounded-md py-1.5 px-2 text-left focus:outline-none text-sm">
46+
<button
47+
type="button"
48+
className="relative items-center w-full gap-2 flex hover:bg-gray-500/10 cursor-pointer rounded-md py-1.5 px-2 text-left focus:outline-none text-sm"
49+
>
4750
{icon ? (
4851
<span className="flex items-center justify-center w-6 h-6 rounded border border-gray-500/20">
4952
{icon}

‎src/components/ShowcaseModerationList.tsx‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
} from 'lucide-react'
2424
import { libraries } from '~/libraries'
2525
import { Badge, Button } from '~/ui'
26+
import { getRowFieldId } from '~/utils/route-encoding'
2627
import { Fragment, useState } from 'react'
2728

2829
interface ShowcaseModerationListProps {
@@ -205,6 +206,11 @@ export function ShowcaseModerationList({
205206
const isExpanded = expandedIds.has(showcase.id)
206207
const isPending = showcase.status === 'pending'
207208
const isModeratingThis = isModeratingId === showcase.id
209+
const moderationNoteId = getRowFieldId(
210+
'showcase-moderation',
211+
showcase.id,
212+
'note',
213+
)
208214

209215
return (
210216
<Fragment key={showcase.id}>
@@ -528,13 +534,13 @@ export function ShowcaseModerationList({
528534
{isPending && (
529535
<div>
530536
<label
531-
htmlFor="note"
537+
htmlFor={moderationNoteId}
532538
className="block text-sm font-semibold mb-2"
533539
>
534540
Internal Moderation Note (optional):
535541
</label>
536542
<textarea
537-
id="note"
543+
id={moderationNoteId}
538544
value={moderationNotes[showcase.id] || ''}
539545
onChange={(e) =>
540546
handleModerationNoteChange(

‎src/components/Tooltip.tsx‎

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,20 @@
11
import * as React from 'react'
22
import {
33
useFloating,
4-
useHover,
5-
useInteractions,
64
FloatingPortal,
75
offset,
86
shift,
97
flip,
108
autoUpdate,
9+
useMergeRefs,
1110
} from '@floating-ui/react'
1211

12+
type TooltipTriggerProps = React.HTMLProps<HTMLElement> &
13+
React.RefAttributes<HTMLElement>
14+
1315
interface TooltipProps {
1416
content: React.ReactNode
15-
children: React.ReactElement
17+
children: React.ReactElement<TooltipTriggerProps>
1618
placement?: 'top' | 'right' | 'bottom' | 'left'
1719
className?: string
1820
}
@@ -25,32 +27,51 @@ export function Tooltip({
2527
}: TooltipProps) {
2628
const [isOpen, setIsOpen] = React.useState(false)
2729

28-
const { refs, floatingStyles, context } = useFloating({
30+
const { refs, floatingStyles } = useFloating<HTMLElement>({
2931
open: isOpen,
3032
onOpenChange: setIsOpen,
3133
placement,
3234
middleware: [offset(5), flip(), shift()],
3335
whileElementsMounted: autoUpdate,
3436
})
3537

36-
const hover = useHover(context)
38+
const triggerRef = useMergeRefs([refs.setReference, children.props.ref])
39+
const triggerProps = children.props
40+
41+
const handleMouseEnter: React.MouseEventHandler<HTMLElement> = (event) => {
42+
triggerProps.onMouseEnter?.(event)
43+
setIsOpen(true)
44+
}
45+
46+
const handleMouseLeave: React.MouseEventHandler<HTMLElement> = (event) => {
47+
triggerProps.onMouseLeave?.(event)
48+
setIsOpen(false)
49+
}
50+
51+
const handleFocus: React.FocusEventHandler<HTMLElement> = (event) => {
52+
triggerProps.onFocus?.(event)
53+
setIsOpen(true)
54+
}
3755

38-
const { getReferenceProps, getFloatingProps } = useInteractions([hover])
56+
const handleBlur: React.FocusEventHandler<HTMLElement> = (event) => {
57+
triggerProps.onBlur?.(event)
58+
setIsOpen(false)
59+
}
3960

4061
return (
4162
<>
42-
{/* eslint-disable-next-line react-hooks/refs */}
4363
{React.cloneElement(children, {
44-
// eslint-disable-next-line react-hooks/refs
45-
ref: refs.setReference,
46-
...getReferenceProps(),
47-
} as any)}
64+
ref: triggerRef,
65+
onMouseEnter: handleMouseEnter,
66+
onMouseLeave: handleMouseLeave,
67+
onFocus: handleFocus,
68+
onBlur: handleBlur,
69+
})}
4870
<FloatingPortal>
4971
{isOpen && (
5072
<div
5173
ref={refs.setFloating /* eslint-disable-line react-hooks/refs */}
5274
style={floatingStyles}
53-
{...getFloatingProps()}
5475
className={`z-50 rounded-md bg-gray-900 px-3 py-1.5 text-sm text-white shadow-lg dark:bg-gray-800 ${className}`}
5576
>
5677
{content}

‎src/components/landing/IntentLanding.tsx‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
intentStatsQueryOptions,
3232
} from '~/queries/intent'
3333
import type { SkillHistoryEntry } from '~/utils/intent.functions'
34+
import { encodePackageNameSlug } from '~/utils/route-encoding'
3435

3536
import { LandingCopyPromptButton } from '~/components/landing/LandingCopyPromptButton'
3637
const library = getLibrary('intent')
@@ -412,7 +413,7 @@ function IntentRegistryPreview() {
412413
<Link
413414
key={pkg.name}
414415
to="/intent/registry/$packageName"
415-
params={{ packageName: pkg.name.replace('/', '__') }}
416+
params={{ packageName: encodePackageNameSlug(pkg.name) }}
416417
className="group flex flex-col gap-2 rounded-lg border border-zinc-200 bg-zinc-50 p-4 transition-colors hover:border-sky-300 dark:border-zinc-800 dark:bg-zinc-900/50 dark:hover:border-sky-700"
417418
>
418419
<div className="mb-1 flex items-start justify-between gap-2">
@@ -430,7 +431,7 @@ function IntentRegistryPreview() {
430431
navigate({
431432
to: '/intent/registry/$packageName',
432433
params: {
433-
packageName: pkg.name.replace('/', '__'),
434+
packageName: encodePackageNameSlug(pkg.name),
434435
},
435436
search: { version: entry.version },
436437
})

‎src/components/shop/ui/Button.tsx‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,12 +23,20 @@ const variants: Record<Variant, string> = {
2323
/** Shop button. Three visual weights — primary, outline, ghost. */
2424
export const ShopButton = React.forwardRef<HTMLButtonElement, Props>(
2525
function ShopButton(
26-
{ variant = 'outline', fullWidth, className, children, ...rest },
26+
{
27+
variant = 'outline',
28+
fullWidth,
29+
type = 'button',
30+
className,
31+
children,
32+
...rest
33+
},
2734
ref,
2835
) {
2936
return (
3037
<button
3138
ref={ref}
39+
type={type}
3240
{...rest}
3341
className={twMerge(
3442
base,

‎src/routes/_library/$libraryId/$version.docs.framework.$framework.{$}[.]md.tsx‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { findLibrary, getBranch } from '~/libraries'
22
import { loadDocs } from '~/utils/docs'
33
import { notFound, createFileRoute } from '@tanstack/react-router'
44
import { getDocsCacheHeaders } from '~/utils/docs-cache-headers'
5+
import { getContentDispositionHeader } from '~/utils/http-response'
56
import { filterFrameworkContent } from '~/utils/markdown/filterFrameworkContent'
67
import { getPackageManager } from '~/utils/markdown/installCommand'
78

@@ -51,13 +52,17 @@ export const Route = createFileRoute(
5152
})
5253

5354
const markdownContent = `# ${doc.title}\n${filteredContent}`
54-
const filename = (docsPath || 'file').split('/').join('-')
55+
const filename = `${docsPath || 'file'}.md`
5556

5657
return new Response(markdownContent, {
5758
headers: {
5859
...cacheHeaders,
5960
'Content-Type': 'text/markdown',
60-
'Content-Disposition': `inline; filename="${filename}.md"`,
61+
'Content-Disposition': getContentDispositionHeader(
62+
'inline',
63+
filename,
64+
'file.md',
65+
),
6166
},
6267
})
6368
},

0 commit comments

Comments
 (0)

Back | FazBrowse Home | New Git URL