test-proxy-recorder 0.3.5 → 0.3.6

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "test-proxy-recorder",
3
- "version": "0.3.5",
3
+ "version": "0.3.6",
4
4
  "description": "HTTP proxy server for recording and replaying network requests in testing. Works seamlessly with Playwright testing framework.",
5
5
  "type": "module",
6
6
  "main": "dist/index.mjs",
@@ -35,28 +35,22 @@
35
35
  "!dist/**/*.test.*",
36
36
  "!dist/**/*.integration.test.*",
37
37
  "README.md",
38
- "LICENSE"
38
+ "LICENSE",
39
+ "skills"
39
40
  ],
40
- "pnpm": {
41
- "onlyBuiltDependencies": ["esbuild", "sharp"]
42
- },
43
- "packageManager": "pnpm@10.20.0+sha512.cf9998222162dd85864d0a8102e7892e7ba4ceadebbf5a31f9c2fce48dfce317a9c53b9f6464d1ef9042cba2e02ae02a9f7c143a2b438cd93c91840f0192b9dd",
44
41
  "scripts": {
45
- "start": "node dist/proxy.js",
46
42
  "dev": "tsx src/proxy.ts",
47
43
  "build": "tsup",
48
- "example:dev": "pnpm --filter example-nextjs16 dev",
49
- "example:build": "pnpm --filter example-nextjs16 build",
50
- "example:services": "pnpm build && pnpm --filter example-nextjs16 start:all",
51
- "example:test:e2e": "pnpm --filter example-nextjs16 test:e2e",
52
- "example:test:e2e:record": "pnpm --filter example-nextjs16 test:e2e:record",
53
- "prepublish": "pnpm run build && pnpm run test:run && pnpm run lint",
54
44
  "lint": "eslint src --ext .ts",
55
45
  "lint:fix": "eslint src --ext .ts --fix",
56
46
  "typecheck": "tsc --noEmit",
57
47
  "test": "vitest",
58
48
  "test:run": "vitest run",
59
- "test:coverage": "vitest run --coverage"
49
+ "test:coverage": "vitest run --coverage",
50
+ "prepare": "tsup",
51
+ "sync-readme": "cp ../../README.md README.md",
52
+ "prepack": "pnpm sync-readme",
53
+ "postpack": "rm -f README.md"
60
54
  },
61
55
  "keywords": [
62
56
  "playwright",
@@ -76,7 +70,8 @@
76
70
  "integration-testing",
77
71
  "test-automation",
78
72
  "api-mocking",
79
- "network-recording"
73
+ "network-recording",
74
+ "tanstack-intent"
80
75
  ],
81
76
  "author": "asmyshlyaev177",
82
77
  "license": "MIT",
@@ -89,7 +84,7 @@
89
84
  },
90
85
  "homepage": "https://github.com/asmyshlyaev177/test-proxy-recorder#readme",
91
86
  "engines": {
92
- "node": ">=22.0.0"
87
+ "node": ">=20.0.0"
93
88
  },
94
89
  "dependencies": {
95
90
  "commander": "^12.0.0",
@@ -101,8 +96,9 @@
101
96
  },
102
97
  "devDependencies": {
103
98
  "@playwright/test": "^1.59.1",
99
+ "@tanstack/intent": "^0.0.41",
104
100
  "@types/http-proxy": "^1.17.15",
105
- "@types/node": "^22.0.0",
101
+ "@types/node": "^20.0.0",
106
102
  "@types/ws": "^8.18.1",
107
103
  "@typescript-eslint/eslint-plugin": "^8.0.0",
108
104
  "@typescript-eslint/parser": "^8.0.0",
@@ -0,0 +1,377 @@
1
+ ---
2
+ name: nextjs-ssr
3
+ description: >
4
+ Wire the x-test-rcrd-id session header through Next.js so server-side fetches
5
+ are recorded under the correct Playwright test session. Covers
6
+ setNextProxyHeaders, createHeadersWithRecordingId, getRecordingId,
7
+ RECORDING_ID_HEADER, middleware.ts (Next.js 13–15), proxy.ts (Next.js 16),
8
+ the React cache() memoization pattern for next/headers, and the axios
9
+ interceptor pattern for SSR requests. Load this skill when setting up
10
+ test-proxy-recorder in a Next.js app that makes server-side API calls.
11
+ type: framework
12
+ library: test-proxy-recorder
13
+ framework: nextjs
14
+ library_version: "0.3.5"
15
+ requires:
16
+ - test-proxy-recorder/proxy-setup
17
+ sources:
18
+ - "asmyshlyaev177/test-proxy-recorder:README.md"
19
+ - "asmyshlyaev177/test-proxy-recorder:packages/test-proxy-recorder/src/nextjs/middleware.ts"
20
+ - "asmyshlyaev177/test-proxy-recorder:apps/example-nextjs16"
21
+ ---
22
+
23
+ This skill builds on test-proxy-recorder/proxy-setup. Read it first for proxy
24
+ CLI setup, playwright.config.ts, and fixtures before applying Next.js patterns.
25
+
26
+ # test-proxy-recorder — Next.js SSR
27
+
28
+ The proxy correlates SSR fetches to the right test session via
29
+ `x-test-rcrd-id`. Playwright sets this header on the browser page automatically
30
+ via `playwrightProxy.before()`. For server-side fetches (SSR, Server
31
+ Components, Route Handlers), the header must be explicitly forwarded through
32
+ every layer.
33
+
34
+ All helpers from `test-proxy-recorder/nextjs` are **no-ops in production**
35
+ (`NODE_ENV=production`) unless `TEST_PROXY_RECORDER_ENABLED=true` is set.
36
+
37
+ ## Setup
38
+
39
+ ### Next.js 13–15 — middleware.ts
40
+
41
+ ```typescript
42
+ // middleware.ts (Next.js 13–15 — at project root)
43
+ import { NextResponse } from 'next/server';
44
+ import type { NextRequest } from 'next/server';
45
+ import { setNextProxyHeaders } from 'test-proxy-recorder/nextjs';
46
+
47
+ export function middleware(request: NextRequest) {
48
+ const response = NextResponse.next();
49
+ setNextProxyHeaders(request, response); // no-op in production
50
+ return response;
51
+ }
52
+
53
+ export const config = {
54
+ matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
55
+ };
56
+ ```
57
+
58
+ ### Next.js 16 — proxy.ts
59
+
60
+ ```typescript
61
+ // proxy.ts (Next.js 16 — at project root, alongside next.config.ts)
62
+ import { NextResponse } from 'next/server';
63
+ import type { NextRequest } from 'next/server';
64
+ import { setNextProxyHeaders } from 'test-proxy-recorder/nextjs';
65
+
66
+ export function proxy(request: NextRequest) {
67
+ const response = NextResponse.next();
68
+ setNextProxyHeaders(request, response);
69
+ return response;
70
+ }
71
+
72
+ export const config = {
73
+ matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
74
+ };
75
+ ```
76
+
77
+ ## Core Patterns
78
+
79
+ ### Inject recording ID into native fetch (Route Handler / Server Component)
80
+
81
+ ```typescript
82
+ // app/api/data/route.ts
83
+ import { headers } from 'next/headers';
84
+ import { createHeadersWithRecordingId } from 'test-proxy-recorder/nextjs';
85
+
86
+ export async function GET(request: Request) {
87
+ const res = await fetch('http://localhost:8100/api/data', {
88
+ cache: 'no-store',
89
+ headers: createHeadersWithRecordingId(await headers(), {
90
+ 'Content-Type': 'application/json',
91
+ }),
92
+ });
93
+ return Response.json(await res.json());
94
+ }
95
+ ```
96
+
97
+ `createHeadersWithRecordingId` merges the session ID into your headers object.
98
+ It is a no-op when the session ID is absent (browser-only tests, or production).
99
+
100
+ ### Memoize header lookup with React cache() (App Router)
101
+
102
+ Avoid calling `headers()` in every individual fetch helper. Wrap it once with
103
+ `React.cache()` so it is called once per request and shared across all
104
+ server-side imports.
105
+
106
+ ```typescript
107
+ // lib/recording-id.ts
108
+ import { cache } from 'react';
109
+ import { RECORDING_ID_HEADER } from 'test-proxy-recorder/nextjs';
110
+
111
+ export const getServerRecordingId = cache(async () => {
112
+ try {
113
+ const { headers } = await import('next/headers');
114
+ const headersList = await headers();
115
+ return headersList.get(RECORDING_ID_HEADER.toLowerCase()) || undefined;
116
+ } catch {
117
+ return undefined; // Not in a Server Component request context
118
+ }
119
+ });
120
+ ```
121
+
122
+ ### Axios interceptor for SSR requests
123
+
124
+ Use this pattern when your app uses axios for server-side API calls instead of
125
+ native `fetch`.
126
+
127
+ ```typescript
128
+ // lib/axios-server.ts
129
+ import axios from 'axios';
130
+ import { RECORDING_ID_HEADER } from 'test-proxy-recorder/nextjs';
131
+ import { getServerRecordingId } from './recording-id';
132
+
133
+ const isTestMode = process.env.NODE_ENV !== 'production';
134
+
135
+ export const axiosForServer = axios.create();
136
+
137
+ axiosForServer.interceptors.request.use(async (config) => {
138
+ if (typeof window === 'undefined' && isTestMode) {
139
+ try {
140
+ const recordingId = await getServerRecordingId();
141
+ if (recordingId) {
142
+ config.headers.set(RECORDING_ID_HEADER, recordingId);
143
+ }
144
+ } catch {
145
+ // Not in a Server Component context — silently skip
146
+ }
147
+ }
148
+ return config;
149
+ });
150
+ ```
151
+
152
+ ### Extract recording ID manually
153
+
154
+ ```typescript
155
+ import { getRecordingId, RECORDING_ID_HEADER } from 'test-proxy-recorder/nextjs';
156
+ import { headers } from 'next/headers';
157
+
158
+ // From headers() in a Server Component
159
+ const recordingId = getRecordingId(await headers());
160
+
161
+ // From NextRequest in middleware
162
+ const recordingId = getRecordingId(request.headers);
163
+
164
+ // Forward manually
165
+ if (recordingId) {
166
+ requestHeaders.set(RECORDING_ID_HEADER, recordingId);
167
+ }
168
+ ```
169
+
170
+ ## Common Mistakes
171
+
172
+ ### HIGH Using middleware.ts (or a middleware export) in Next.js 16
173
+
174
+ Wrong:
175
+ ```typescript
176
+ // middleware.ts — ignored in Next.js 16, session header never forwarded
177
+ import { setNextProxyHeaders } from 'test-proxy-recorder/nextjs';
178
+ export function middleware(request) {
179
+ const response = NextResponse.next();
180
+ setNextProxyHeaders(request, response);
181
+ return response;
182
+ }
183
+ ```
184
+
185
+ Correct:
186
+ ```typescript
187
+ // proxy.ts — Next.js 16 middleware entry point, at project root
188
+ import { setNextProxyHeaders } from 'test-proxy-recorder/nextjs';
189
+ export function proxy(request) {
190
+ const response = NextResponse.next();
191
+ setNextProxyHeaders(request, response);
192
+ return response;
193
+ }
194
+ export const config = {
195
+ matcher: ['/((?!_next/static|_next/image|favicon.ico).*)'],
196
+ };
197
+ ```
198
+
199
+ Next.js 16 replaced `middleware.ts` with `proxy.ts` as the middleware entry
200
+ point, and the exported function is named `proxy`, not `middleware`. Keeping
201
+ either old name silently does nothing — the session header is never forwarded
202
+ and all SSR recordings are grouped under the wrong session.
203
+
204
+ Source: apps/example-nextjs16/proxy.ts
205
+
206
+ ---
207
+
208
+ ### HIGH setNextProxyHeaders set but SSR fetches still missing the header
209
+
210
+ Wrong:
211
+ ```typescript
212
+ // middleware.ts — header set on response, but individual fetch calls don't use it
213
+ export function middleware(request) {
214
+ const response = NextResponse.next();
215
+ setNextProxyHeaders(request, response);
216
+ return response;
217
+ }
218
+
219
+ // app/api/data/route.ts — header not forwarded to outgoing fetch
220
+ export async function GET() {
221
+ const data = await fetch('http://localhost:8100/api/data');
222
+ return Response.json(await data.json());
223
+ }
224
+ ```
225
+
226
+ Correct:
227
+ ```typescript
228
+ // app/api/data/route.ts — explicitly inject header into each outgoing fetch
229
+ import { headers } from 'next/headers';
230
+ import { createHeadersWithRecordingId } from 'test-proxy-recorder/nextjs';
231
+
232
+ export async function GET() {
233
+ const data = await fetch('http://localhost:8100/api/data', {
234
+ headers: createHeadersWithRecordingId(await headers()),
235
+ });
236
+ return Response.json(await data.json());
237
+ }
238
+ ```
239
+
240
+ `setNextProxyHeaders` makes the session ID available to server components via
241
+ `next/headers`. It does **not** automatically inject the header into outgoing
242
+ fetch calls — each server-side fetch must use `createHeadersWithRecordingId()`
243
+ explicitly.
244
+
245
+ Source: README.md — Manual header forwarding; channels/web/app/api
246
+
247
+ ---
248
+
249
+ ### HIGH Recording against a production build without TEST_PROXY_RECORDER_ENABLED
250
+
251
+ Wrong:
252
+ ```json
253
+ {
254
+ "scripts": {
255
+ "start:proxy": "concurrently \"pnpm proxy\" \"INTERNAL_API_URL=http://localhost:8100 next start\""
256
+ }
257
+ }
258
+ ```
259
+
260
+ Correct:
261
+ ```json
262
+ {
263
+ "scripts": {
264
+ "start:proxy": "concurrently \"pnpm proxy\" \"INTERNAL_API_URL=http://localhost:8100 TEST_PROXY_RECORDER_ENABLED=true next start\""
265
+ }
266
+ }
267
+ ```
268
+
269
+ Recording should run against a production build (`next build && next start` —
270
+ see proxy-setup), but `next build` sets `NODE_ENV=production`, which turns
271
+ `setNextProxyHeaders` and `createHeadersWithRecordingId` into silent no-ops.
272
+ SSR requests still flow through the proxy but lose their session ID, so they
273
+ are recorded under the wrong session — or not at all. Set
274
+ `TEST_PROXY_RECORDER_ENABLED=true` on the app process whenever testing a
275
+ production build.
276
+
277
+ Source: packages/test-proxy-recorder/src/nextjs/middleware.ts — isRecorderEnabled()
278
+
279
+ ---
280
+
281
+ ### MEDIUM Importing next/headers at module level in an axios interceptor
282
+
283
+ Wrong:
284
+ ```typescript
285
+ import { headers } from 'next/headers'; // throws when evaluated on the client
286
+
287
+ axiosForServer.interceptors.request.use(async (config) => {
288
+ const id = (await headers()).get('x-test-rcrd-id');
289
+ config.headers.set('x-test-rcrd-id', id);
290
+ return config;
291
+ });
292
+ ```
293
+
294
+ Correct:
295
+ ```typescript
296
+ axiosForServer.interceptors.request.use(async (config) => {
297
+ if (typeof window === 'undefined' && isTestMode) {
298
+ try {
299
+ const { getServerRecordingId } = await import('./recording-id');
300
+ const recordingId = await getServerRecordingId();
301
+ if (recordingId) config.headers.set(RECORDING_ID_HEADER, recordingId);
302
+ } catch {
303
+ // Not in a Server Component context — silently skip
304
+ }
305
+ }
306
+ return config;
307
+ });
308
+ ```
309
+
310
+ `next/headers` throws when imported outside a Server Component request context
311
+ (including on the client). Always lazy-import it inside the interceptor, guard
312
+ with `typeof window === 'undefined'`, and wrap in try/catch.
313
+
314
+ Source: channels/web/core/api/axios.ts
315
+
316
+ ---
317
+
318
+ ### MEDIUM Calling headers() on every SSR fetch instead of caching per request
319
+
320
+ Wrong:
321
+ ```typescript
322
+ // Called independently in each server utility — redundant async header reads
323
+ async function fetchUsers() {
324
+ const { headers } = await import('next/headers');
325
+ const id = (await headers()).get('x-test-rcrd-id');
326
+ return fetch(url, { headers: { 'x-test-rcrd-id': id ?? '' } });
327
+ }
328
+ async function fetchPosts() {
329
+ const { headers } = await import('next/headers');
330
+ const id = (await headers()).get('x-test-rcrd-id');
331
+ return fetch(url2, { headers: { 'x-test-rcrd-id': id ?? '' } });
332
+ }
333
+ ```
334
+
335
+ Correct:
336
+ ```typescript
337
+ // lib/recording-id.ts — memoized once per request via React cache()
338
+ import { cache } from 'react';
339
+ import { RECORDING_ID_HEADER } from 'test-proxy-recorder/nextjs';
340
+ export const getServerRecordingId = cache(async () => { /* ... */ });
341
+
342
+ // Each utility reuses the cached value
343
+ async function fetchUsers() {
344
+ const id = await getServerRecordingId();
345
+ return fetch(url, { headers: id ? { [RECORDING_ID_HEADER]: id } : {} });
346
+ }
347
+ ```
348
+
349
+ Wrap the `headers()` call in `React.cache()` once. The memoized function is
350
+ called once per server request regardless of how many fetch utilities invoke it.
351
+
352
+ Source: channels/web/lib/recording-id.ts
353
+
354
+ ---
355
+
356
+ ### MEDIUM Manually setting header without production guard
357
+
358
+ Wrong:
359
+ ```typescript
360
+ // No production guard — leaks session IDs in prod if env var is misconfigured
361
+ response.headers.set('x-test-rcrd-id', request.headers.get('x-test-rcrd-id') ?? '');
362
+ ```
363
+
364
+ Correct:
365
+ ```typescript
366
+ import { setNextProxyHeaders } from 'test-proxy-recorder/nextjs';
367
+ setNextProxyHeaders(request, response); // automatically skips in production
368
+ ```
369
+
370
+ Use the library helpers instead of manually reading/setting `x-test-rcrd-id`.
371
+ `setNextProxyHeaders` and `createHeadersWithRecordingId` both check
372
+ `NODE_ENV !== 'production'` (or `TEST_PROXY_RECORDER_ENABLED`) and are no-ops
373
+ when the guard fails.
374
+
375
+ Source: packages/test-proxy-recorder/src/nextjs/middleware.ts — isRecorderEnabled()
376
+
377
+ See also: test-proxy-recorder/proxy-setup — for proxy CLI, fixtures, and record/replay lifecycle