@jgamaraalv/ts-dev-kit 2.2.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/.claude-plugin/marketplace.json +3 -3
  2. package/.claude-plugin/plugin.json +2 -2
  3. package/CHANGELOG.md +52 -0
  4. package/README.md +106 -50
  5. package/package.json +2 -2
  6. package/skills/codebase-adapter/SKILL.md +237 -0
  7. package/skills/codebase-adapter/template.md +25 -0
  8. package/skills/conventional-commits/SKILL.md +12 -0
  9. package/skills/core-web-vitals/SKILL.md +102 -0
  10. package/skills/core-web-vitals/references/cls.md +154 -0
  11. package/skills/core-web-vitals/references/inp.md +140 -0
  12. package/skills/core-web-vitals/references/lcp.md +89 -0
  13. package/skills/core-web-vitals/references/tools.md +112 -0
  14. package/skills/core-web-vitals/scripts/visualize.py +222 -0
  15. package/skills/debug/SKILL.md +10 -27
  16. package/skills/debug/template.md +23 -0
  17. package/skills/{task → execute-task}/SKILL.md +78 -50
  18. package/skills/generate-prd/SKILL.md +56 -0
  19. package/skills/generate-prd/template.md +69 -0
  20. package/skills/generate-task/SKILL.md +136 -0
  21. package/skills/generate-task/template.md +71 -0
  22. package/skills/owasp-security-review/SKILL.md +1 -10
  23. package/skills/owasp-security-review/template.md +6 -0
  24. package/skills/tanstack-query/SKILL.md +348 -0
  25. package/skills/tanstack-query/references/advanced-patterns.md +376 -0
  26. package/skills/tanstack-query/references/api-reference.md +297 -0
  27. package/skills/tanstack-query/references/ssr-nextjs.md +272 -0
  28. package/skills/tanstack-query/references/testing.md +175 -0
  29. package/skills/ui-ux-guidelines/SKILL.md +1 -17
  30. package/skills/ui-ux-guidelines/template.md +15 -0
  31. /package/skills/{task → execute-task}/references/agent-dispatch.md +0 -0
  32. /package/skills/{task → execute-task}/references/verification-protocol.md +0 -0
  33. /package/skills/{task/references/output-templates.md → execute-task/template.md} +0 -0
@@ -0,0 +1,272 @@
1
+ # TanStack Query v5 — SSR & Next.js
2
+
3
+ ## Table of Contents
4
+ - [Core Concept](#core-concept)
5
+ - [Next.js Pages Router](#nextjs-pages-router)
6
+ - [Next.js App Router](#nextjs-app-router)
7
+ - [Streaming](#streaming)
8
+ - [Critical Gotchas](#critical-gotchas)
9
+
10
+ ## Core Concept
11
+
12
+ Server rendering with TanStack Query follows three steps:
13
+ 1. **Prefetch** data on the server with `queryClient.prefetchQuery()`
14
+ 2. **Dehydrate** the cache with `dehydrate(queryClient)`
15
+ 3. **Hydrate** on the client with `<HydrationBoundary state={dehydratedState}>`
16
+
17
+ ## Next.js Pages Router
18
+
19
+ ### getStaticProps / getServerSideProps
20
+
21
+ ```tsx
22
+ import { dehydrate, QueryClient } from '@tanstack/react-query'
23
+
24
+ export async function getStaticProps() {
25
+ const queryClient = new QueryClient()
26
+
27
+ await queryClient.prefetchQuery({
28
+ queryKey: ['posts'],
29
+ queryFn: getPosts,
30
+ })
31
+
32
+ return {
33
+ props: {
34
+ dehydratedState: dehydrate(queryClient),
35
+ },
36
+ }
37
+ }
38
+ ```
39
+
40
+ ### _app.tsx setup
41
+
42
+ ```tsx
43
+ import { HydrationBoundary, QueryClient, QueryClientProvider } from '@tanstack/react-query'
44
+ import { useState } from 'react'
45
+
46
+ export default function MyApp({ Component, pageProps }) {
47
+ const [queryClient] = useState(() => new QueryClient({
48
+ defaultOptions: {
49
+ queries: { staleTime: 60 * 1000 },
50
+ },
51
+ }))
52
+
53
+ return (
54
+ <QueryClientProvider client={queryClient}>
55
+ <HydrationBoundary state={pageProps.dehydratedState}>
56
+ <Component {...pageProps} />
57
+ </HydrationBoundary>
58
+ </QueryClientProvider>
59
+ )
60
+ }
61
+ ```
62
+
63
+ ### Page component — uses useQuery normally
64
+
65
+ ```tsx
66
+ export default function PostsPage() {
67
+ const { data } = useQuery({ queryKey: ['posts'], queryFn: getPosts })
68
+ // data is immediately available from the hydrated cache
69
+ }
70
+ ```
71
+
72
+ ## Next.js App Router
73
+
74
+ ### Provider setup (app/providers.tsx)
75
+
76
+ ```tsx
77
+ 'use client'
78
+
79
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
80
+ import { useState } from 'react'
81
+
82
+ function makeQueryClient() {
83
+ return new QueryClient({
84
+ defaultOptions: {
85
+ queries: {
86
+ staleTime: 60 * 1000, // must be > 0 for SSR
87
+ },
88
+ },
89
+ })
90
+ }
91
+
92
+ let browserQueryClient: QueryClient | undefined
93
+
94
+ function getQueryClient() {
95
+ if (typeof window === 'undefined') {
96
+ // Server: always make a new query client
97
+ return makeQueryClient()
98
+ }
99
+ // Browser: reuse singleton
100
+ if (!browserQueryClient) browserQueryClient = makeQueryClient()
101
+ return browserQueryClient
102
+ }
103
+
104
+ export default function Providers({ children }: { children: React.ReactNode }) {
105
+ const queryClient = getQueryClient()
106
+
107
+ return (
108
+ <QueryClientProvider client={queryClient}>
109
+ {children}
110
+ </QueryClientProvider>
111
+ )
112
+ }
113
+ ```
114
+
115
+ **Why not `useState`?** If there is no Suspense boundary between the provider and suspending code, React throws away state on initial suspend. The singleton pattern avoids losing the client.
116
+
117
+ ### Root layout (app/layout.tsx)
118
+
119
+ ```tsx
120
+ import Providers from './providers'
121
+
122
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
123
+ return (
124
+ <html>
125
+ <body>
126
+ <Providers>{children}</Providers>
127
+ </body>
128
+ </html>
129
+ )
130
+ }
131
+ ```
132
+
133
+ ### Prefetch in Server Components
134
+
135
+ ```tsx
136
+ // app/posts/page.tsx (Server Component)
137
+ import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query'
138
+ import Posts from './posts' // Client Component
139
+
140
+ export default async function PostsPage() {
141
+ const queryClient = new QueryClient()
142
+
143
+ await queryClient.prefetchQuery({
144
+ queryKey: ['posts'],
145
+ queryFn: getPosts,
146
+ })
147
+
148
+ return (
149
+ <HydrationBoundary state={dehydrate(queryClient)}>
150
+ <Posts />
151
+ </HydrationBoundary>
152
+ )
153
+ }
154
+ ```
155
+
156
+ ```tsx
157
+ // app/posts/posts.tsx (Client Component)
158
+ 'use client'
159
+
160
+ import { useQuery } from '@tanstack/react-query'
161
+
162
+ export default function Posts() {
163
+ const { data } = useQuery({ queryKey: ['posts'], queryFn: getPosts })
164
+ // data is available immediately from hydration
165
+ }
166
+ ```
167
+
168
+ ### Nested prefetching
169
+
170
+ Multiple `<HydrationBoundary>` with separate queryClients at different levels is fine. Deeply nested Server Components can each prefetch their own data.
171
+
172
+ ### Shared queryClient with React.cache
173
+
174
+ ```tsx
175
+ import { cache } from 'react'
176
+
177
+ const getQueryClient = cache(() => new QueryClient())
178
+
179
+ // Any Server Component in the same request:
180
+ export default async function Page() {
181
+ const queryClient = getQueryClient()
182
+ await queryClient.prefetchQuery({ queryKey: ['posts'], queryFn: getPosts })
183
+ return (
184
+ <HydrationBoundary state={dehydrate(queryClient)}>
185
+ <Posts />
186
+ </HydrationBoundary>
187
+ )
188
+ }
189
+ ```
190
+
191
+ Downside: each `dehydrate()` serializes the entire cache, including previously serialized queries.
192
+
193
+ ## Streaming
194
+
195
+ Available since v5.40.0. Pending queries can be dehydrated and streamed without `await`.
196
+
197
+ ### Setup — configure dehydration to include pending queries
198
+
199
+ ```tsx
200
+ function makeQueryClient() {
201
+ return new QueryClient({
202
+ defaultOptions: {
203
+ queries: { staleTime: 60 * 1000 },
204
+ dehydrate: {
205
+ shouldDehydrateQuery: (query) =>
206
+ defaultShouldDehydrateQuery(query) || query.state.status === 'pending',
207
+ },
208
+ },
209
+ })
210
+ }
211
+ ```
212
+
213
+ ### Stream prefetch (no await)
214
+
215
+ ```tsx
216
+ // app/posts/page.tsx
217
+ export default function PostsPage() {
218
+ const queryClient = new QueryClient()
219
+
220
+ // No await — query starts but doesn't block rendering
221
+ queryClient.prefetchQuery({ queryKey: ['posts'], queryFn: getPosts })
222
+
223
+ return (
224
+ <HydrationBoundary state={dehydrate(queryClient)}>
225
+ <Posts />
226
+ </HydrationBoundary>
227
+ )
228
+ }
229
+ ```
230
+
231
+ On the client, use `useSuspenseQuery` to consume the streamed promise:
232
+
233
+ ```tsx
234
+ 'use client'
235
+ export default function Posts() {
236
+ const { data } = useSuspenseQuery({ queryKey: ['posts'], queryFn: getPosts })
237
+ // Suspends until streamed data arrives, then renders
238
+ }
239
+ ```
240
+
241
+ ### Non-JSON serialization
242
+
243
+ For data with Dates, Maps, etc., configure custom serialization:
244
+
245
+ ```tsx
246
+ defaultOptions: {
247
+ dehydrate: { serializeData: superjson.serialize },
248
+ hydrate: { deserializeData: superjson.deserialize },
249
+ }
250
+ ```
251
+
252
+ ## Critical Gotchas
253
+
254
+ 1. **Never create QueryClient at module scope** — it leaks data between requests/users:
255
+ ```tsx
256
+ // WRONG
257
+ const queryClient = new QueryClient()
258
+ export default function App() { ... }
259
+
260
+ // CORRECT
261
+ const [queryClient] = useState(() => new QueryClient())
262
+ ```
263
+
264
+ 2. **Set `staleTime > 0` for SSR** — with default `staleTime: 0`, the client immediately refetches on mount, negating the prefetch.
265
+
266
+ 3. **Don't use `fetchQuery` in Server Components to render data** — if both a Server Component and a Client Component use the same data, they desync when the client revalidates. Use `prefetchQuery` (which only populates the cache) and let client components own the data rendering.
267
+
268
+ 4. **`hydrate` only overwrites if incoming data is newer** — checked by timestamp.
269
+
270
+ 5. **`Error` objects and `undefined` are not JSON-serializable** — handle serialization manually if dehydrating errors. By default, errors are redacted (replaced with `null`).
271
+
272
+ 6. **Data ownership rule:** The component that renders the data should own it. Server Components prefetch; Client Components consume via `useQuery`/`useSuspenseQuery`.
@@ -0,0 +1,175 @@
1
+ # TanStack Query v5 — Testing
2
+
3
+ ## Setup
4
+
5
+ Use `@testing-library/react` (v14+) directly — no need for `@testing-library/react-hooks`.
6
+
7
+ ### Create a wrapper with isolated QueryClient
8
+
9
+ ```tsx
10
+ import { renderHook, waitFor } from '@testing-library/react'
11
+ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
12
+ import type { ReactNode } from 'react'
13
+
14
+ function createTestQueryClient() {
15
+ return new QueryClient({
16
+ defaultOptions: {
17
+ queries: {
18
+ retry: false, // CRITICAL: disable retries in tests
19
+ },
20
+ },
21
+ })
22
+ }
23
+
24
+ function createWrapper() {
25
+ const queryClient = createTestQueryClient()
26
+ return function Wrapper({ children }: { children: ReactNode }) {
27
+ return (
28
+ <QueryClientProvider client={queryClient}>
29
+ {children}
30
+ </QueryClientProvider>
31
+ )
32
+ }
33
+ }
34
+ ```
35
+
36
+ ## Testing a Custom Hook
37
+
38
+ ```tsx
39
+ // hooks/useTodos.ts
40
+ export function useTodos() {
41
+ return useQuery({ queryKey: ['todos'], queryFn: fetchTodos })
42
+ }
43
+
44
+ // hooks/useTodos.test.ts
45
+ import { renderHook, waitFor } from '@testing-library/react'
46
+ import { useTodos } from './useTodos'
47
+
48
+ test('fetches todos', async () => {
49
+ const { result } = renderHook(() => useTodos(), {
50
+ wrapper: createWrapper(),
51
+ })
52
+
53
+ await waitFor(() => expect(result.current.isSuccess).toBe(true))
54
+ expect(result.current.data).toEqual([{ id: 1, title: 'Test' }])
55
+ })
56
+ ```
57
+
58
+ ## Network Mocking
59
+
60
+ ### With nock
61
+
62
+ ```tsx
63
+ import nock from 'nock'
64
+
65
+ test('fetches data from API', async () => {
66
+ nock('http://example.com')
67
+ .get('/api/todos')
68
+ .reply(200, [{ id: 1, title: 'Test' }])
69
+
70
+ const { result } = renderHook(() => useTodos(), {
71
+ wrapper: createWrapper(),
72
+ })
73
+
74
+ await waitFor(() => expect(result.current.isSuccess).toBe(true))
75
+ expect(result.current.data).toEqual([{ id: 1, title: 'Test' }])
76
+ })
77
+ ```
78
+
79
+ ### With msw (recommended)
80
+
81
+ ```tsx
82
+ import { setupServer } from 'msw/node'
83
+ import { http, HttpResponse } from 'msw'
84
+
85
+ const server = setupServer(
86
+ http.get('/api/todos', () =>
87
+ HttpResponse.json([{ id: 1, title: 'Test' }])
88
+ ),
89
+ )
90
+
91
+ beforeAll(() => server.listen())
92
+ afterEach(() => server.resetHandlers())
93
+ afterAll(() => server.close())
94
+ ```
95
+
96
+ ## Testing Infinite Queries
97
+
98
+ ```tsx
99
+ import nock from 'nock'
100
+
101
+ test('fetches multiple pages', async () => {
102
+ const expectation = nock('http://example.com')
103
+ .persist() // .persist() for multiple requests
104
+ .query(true)
105
+ .get('/api/items')
106
+ .reply(200, (uri) => {
107
+ const url = new URL(`http://example.com${uri}`)
108
+ const page = url.searchParams.get('page') ?? '1'
109
+ return { items: [`item-${page}`], nextPage: Number(page) < 3 ? Number(page) + 1 : null }
110
+ })
111
+
112
+ const { result } = renderHook(() => useInfiniteItems(), {
113
+ wrapper: createWrapper(),
114
+ })
115
+
116
+ await waitFor(() => expect(result.current.isSuccess).toBe(true))
117
+ expect(result.current.data.pages).toHaveLength(1)
118
+
119
+ // Fetch next page
120
+ result.current.fetchNextPage()
121
+
122
+ await waitFor(() => expect(result.current.data.pages).toHaveLength(2))
123
+ })
124
+ ```
125
+
126
+ ## Testing Mutations
127
+
128
+ ```tsx
129
+ test('creates a todo and invalidates list', async () => {
130
+ const queryClient = createTestQueryClient()
131
+ const wrapper = ({ children }: { children: ReactNode }) => (
132
+ <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
133
+ )
134
+
135
+ // Pre-populate cache
136
+ queryClient.setQueryData(['todos'], [{ id: 1, title: 'Existing' }])
137
+
138
+ const { result } = renderHook(() => useCreateTodo(), { wrapper })
139
+
140
+ result.current.mutate({ title: 'New Todo' })
141
+
142
+ await waitFor(() => expect(result.current.isSuccess).toBe(true))
143
+ })
144
+ ```
145
+
146
+ ## Key Gotchas
147
+
148
+ 1. **Always disable retries** — Default 3 retries with exponential backoff causes test timeouts:
149
+ ```tsx
150
+ new QueryClient({ defaultOptions: { queries: { retry: false } } })
151
+ ```
152
+ Note: explicit `retry` on individual queries overrides this default.
153
+
154
+ 2. **Always use `waitFor`** — Query state updates are async. Never assert synchronously after `renderHook`.
155
+
156
+ 3. **Isolate QueryClient per test** — Create a new `QueryClient` for each test to prevent cross-test contamination. Do not share a client across tests.
157
+
158
+ 4. **Jest "did not exit" warning** — If you get this, set `gcTime: Infinity` on the test QueryClient to prevent garbage collection timers from hanging:
159
+ ```tsx
160
+ new QueryClient({
161
+ defaultOptions: {
162
+ queries: { retry: false, gcTime: Infinity },
163
+ },
164
+ })
165
+ ```
166
+
167
+ 5. **Component testing** — For testing components (not just hooks), render the component inside the wrapper:
168
+ ```tsx
169
+ import { render, screen, waitFor } from '@testing-library/react'
170
+
171
+ test('renders todo list', async () => {
172
+ render(<TodoList />, { wrapper: createWrapper() })
173
+ await waitFor(() => expect(screen.getByText('Test Todo')).toBeInTheDocument())
174
+ })
175
+ ```
@@ -76,23 +76,7 @@ Dispatch hub for UI/UX rules. Load the relevant reference file for full details.
76
76
 
77
77
  Group findings by file. Use `file:line` format (VS Code clickable). Be terse -- state issue and location. Skip explanation unless fix is non-obvious.
78
78
 
79
- ```text
80
- ## src/Button.tsx
81
-
82
- src/Button.tsx:42 - icon button missing aria-label
83
- src/Button.tsx:18 - input lacks label
84
- src/Button.tsx:55 - animation missing prefers-reduced-motion
85
- src/Button.tsx:67 - transition: all -> list properties explicitly
86
-
87
- ## src/Modal.tsx
88
-
89
- src/Modal.tsx:12 - missing overscroll-behavior: contain
90
- src/Modal.tsx:34 - "..." -> "..."
91
-
92
- ## src/Card.tsx
93
-
94
- pass
95
- ```
79
+ See [template.md](template.md) for the expected output format.
96
80
 
97
81
  ---
98
82
 
@@ -0,0 +1,15 @@
1
+ ## src/Button.tsx
2
+
3
+ src/Button.tsx:42 - icon button missing aria-label
4
+ src/Button.tsx:18 - input lacks label
5
+ src/Button.tsx:55 - animation missing prefers-reduced-motion
6
+ src/Button.tsx:67 - transition: all -> list properties explicitly
7
+
8
+ ## src/Modal.tsx
9
+
10
+ src/Modal.tsx:12 - missing overscroll-behavior: contain
11
+ src/Modal.tsx:34 - "Close" -> "Close dialog"
12
+
13
+ ## src/Card.tsx
14
+
15
+ pass