rsc-tape
v0.1.0
Published
Intercept React Server Actions and generate MSW mock handlers
Maintainers
Readme
📼 rsc-tape
Record React Server Actions. Replay them with MSW.
Capture every Server Action from Next.js, Waku, Parcel, or any RSC server — zero config, zero framework hooks.
✨ Why rsc-tape?
Testing Server Actions is painful. You need real responses to write meaningful tests, but manually crafting RSC payloads is tedious and error-prone.
rsc-tape solves this by recording real interactions from your dev server and generating MSW handlers you can drop straight into your test suite.
Dev Server → rsc-tape records → JSON fixtures → MSW handlers → Your tests🚀 Quick start
npm install rsc-tape --save-devnpx rsctape init # Generate config + framework entry point
# ... start dev server, use your app ...
npx rsctape mock -o ./src/mocks/handlers.ts # Generate MSW handlersThat's it. Your test suite now has real Server Action mocks.
📦 Framework setup
Add to instrumentation.ts (created by rsctape init):
export async function register() {
if (process.env.NODE_ENV === 'development') {
const { register } = await import('rsc-tape');
register();
}
}Add to your entry point:
if (process.env.NODE_ENV === 'development') {
const { register } = await import('rsc-tape');
register();
}import { register } from 'rsc-tape';
register();
// ... your http.createServer() call💡 rsc-tape only activates when
NODE_ENV=developmentorRSCTAPE_ENABLED=true. Zero overhead in production.
🛠 CLI
| Command | Description |
|:--------|:------------|
| rsctape init | 🔍 Detect framework, generate config and entry point |
| rsctape list | 📋 List all captured fixtures |
| rsctape mock -o file.ts | ⚡ Generate MSW handlers |
| rsctape mock -o file.ts --watch | 👀 Auto-regenerate on fixture changes |
| rsctape diff <id1> <id2> | 🔄 Compare two fixtures (input fields) |
| rsctape diff <id1> <id2> --full | 📝 Include RSC Payload line-by-line diff |
| rsctape types | 🏷️ Generate TypeScript types from fixtures |
| rsctape types --jsdoc | 📄 Generate JSDoc types instead |
| rsctape delete <id> | 🗑️ Delete a fixture |
Common flags
-d, --dir <path> Override fixture directory
-o, --output <path> Output file (mock)
--actions <ids...> Filter by action IDs (mock)
--full Full output diff (diff)
--jsdoc JSDoc output (types)
-w, --watch Watch mode (mock)⚙️ How it works
┌─────────────┐ ┌──────────────┐ ┌──────────────┐
│ Browser │────▶│ Node.js │────▶│ Your App │
│ Client │ │ HTTP Server │ │ Handler │
└─────────────┘ └──────┬───────┘ └──────────────┘
│
┌──────▼───────┐
│ rsc-tape │ ← monkey-patches http.createServer
│ interceptor │
└──────┬───────┘
│
┌────────────▼────────────┐
│ Next-Action header? │
│ Yes → buffer & save │
│ No → pass through │
└─────────────────────────┘register()patcheshttp.createServerto wrap your request handler- Checks each request for the
Next-Actionheader (RSC protocol standard) - Buffers request body + response chunks without modifying them
- After
res.end(), asynchronously parses FormData and saves fixtures rsctape mockreads fixtures and generates MSW handlers
The interceptor is purely observational — it never modifies request or response data.
📁 Configuration
rsctape.config.json:
{
"fixtureDir": "./fixtures/actions",
"ignore": ["**/internal-*"]
}| Field | Default | Description |
|:------|:--------|:------------|
| fixtureDir | ./fixtures/actions | Where fixtures are saved |
| ignore | [] | Glob patterns for action IDs to skip |
Environment variables
| Variable | Effect |
|:---------|:-------|
| NODE_ENV=development | Enable interception (default) |
| RSCTAPE_ENABLED=true | Force enable regardless of NODE_ENV |
| RSCTAPE_VERBOSE=true | Log each captured action to console |
📼 Fixture format
Each Server Action produces two files:
{actionId}.json
{
"input": {
"username": "alice",
"profile": {
"age": 25,
"city": "Taipei"
}
},
"output": "0:{\"result\":\"ok\"}\n"
}{actionId}.meta.json
{
"actionId": "abc123def",
"url": "/",
"method": "POST",
"statusCode": 200,
"contentType": "text/x-component",
"timestamp": "2024-01-15T10:30:00Z",
"formDataMetadata": {
"invocationType": "form",
"frameworkHint": "next"
}
}⚡ Generated MSW handlers
import { http, HttpResponse } from 'msw';
/** Handler for action: abc123 */
export const handle_abc123 = http.post('*', ({ request }) => {
if (request.headers.get('Next-Action') !== 'abc123') return;
return new HttpResponse(`0:{"result":"ok"}\n`, {
headers: { 'Content-Type': 'text/x-component' },
});
});
export const handlers = [handle_abc123];Handlers use
http.post('*')with header matching because Server Action URLs vary by framework — theNext-Actionheader is the stable identifier.
🏷️ Type generation
rsctape types infers types from fixture data:
| Invocation type | Output |
|:----------------|:-------|
| Form submission | TypeScript interface from form fields |
| Programmatic call | TypeScript tuple from serialized args |
// Form submission → interface
export interface CreateUserInput {
username: string;
age: number;
}
// Programmatic call → tuple
export type UpdateProfileInput = [string, Record<string, unknown>];🔍 FormData parsing
rsc-tape handles the full complexity of Server Action FormData:
| Pattern | Result |
|:--------|:-------|
| user[name] | { user: { name: "..." } } |
| tags[] | ["a", "b"] |
| items[0], items[1] | ["first", "second"] |
| Duplicate keys | Collected as arrays |
| File fields | { __type: "file", name, type, size } |
| JSON string values | Auto-parsed |
| $ACTION_ID_, $ACTION_REF_ | Separated into metadata |
| 1_$ACTION_ID_xxx | Ordered args array |
🔄 Diff
Action IDs change on HMR/recompile, but old fixtures stay on disk:
rsctape diff abc123 def456 # Input structure diff
rsctape diff abc123 def456 --full # + RSC Payload line diff💻 Programmatic API
import { register, createHandler, generateHandlers, detectFramework } from 'rsc-tape';
register({ fixtureDir: './my-fixtures', verbose: true });
const code = createHandler('actionId', fixture);
const module = await generateHandlers({
fixtureDir: './fixtures/actions',
outputPath: './handlers.ts',
});
const framework = await detectFramework(); // 'next' | 'waku' | 'parcel' | 'unknown'🤝 Relationship to api-tape
rsc-tape is the RSC companion to api-tape. It reuses api-tape's core utilities (diff, type inference, sanitization) and adds Server Action-specific logic: HTTP interception, FormData parsing, and Next-Action header-based MSW handlers.
