Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
root = true

[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[*.md]
trim_trailing_whitespace = false
39 changes: 39 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: CI

on:
push:
branches: [main]
pull_request:
branches: [main]

permissions:
contents: read

jobs:
ci:
strategy:
fail-fast: false
matrix:
node-version: [20, 22]
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4

- uses: pnpm/action-setup@v4

- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: pnpm

- run: pnpm install --frozen-lockfile

- name: Typecheck
run: pnpm -r typecheck

- name: Lint
run: pnpm -r lint

- name: Test
run: pnpm -r test
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
node_modules/
dist/
*.tgz
.DS_Store
*.log
coverage/
.vite/
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# httptape-js

JavaScript/TypeScript SDK for [httptape](https://github.com/httptape/httptape) -- HTTP traffic recording, sanitization, and replay.

## Packages

| Package | Description |
|---|---|
| [`vite-plugin-httptape`](./packages/vite-plugin-httptape/) | Vite plugin that manages an httptape process during `vite dev` |
| `@httptape/binary-*` | Platform-specific httptape binaries (installed automatically) |

## Monorepo structure

This is a [pnpm workspace](https://pnpm.io/workspaces) monorepo. The Vite plugin is the primary user-facing package. The `@httptape/binary-*` packages are platform-specific wrappers around the httptape Go binary, installed automatically as optional dependencies.

## Development

```bash
pnpm install
pnpm typecheck
pnpm lint
pnpm test
```

## License

Apache-2.0
13 changes: 13 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"private": true,
"type": "module",
"packageManager": "pnpm@10.33.4",
"engines": {
"node": ">=20"
},
"scripts": {
"typecheck": "pnpm -r typecheck",
"lint": "pnpm -r lint",
"test": "pnpm -r test"
}
}
1 change: 1 addition & 0 deletions packages/binary-darwin-arm64/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Platform-specific httptape binary for macOS ARM64 (Apple Silicon). Do not install directly -- use `vite-plugin-httptape` instead.
Empty file.
10 changes: 10 additions & 0 deletions packages/binary-darwin-arm64/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "@httptape/binary-darwin-arm64",
"version": "0.0.1",
"description": "Platform-specific httptape binary for macOS ARM64 (Apple Silicon)",
"license": "Apache-2.0",
"os": ["darwin"],
"cpu": ["arm64"],
"files": ["bin/"],
"preferUnplugged": true
}
1 change: 1 addition & 0 deletions packages/binary-darwin-x64/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Platform-specific httptape binary for macOS x64 (Intel). Do not install directly -- use `vite-plugin-httptape` instead.
Empty file.
10 changes: 10 additions & 0 deletions packages/binary-darwin-x64/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "@httptape/binary-darwin-x64",
"version": "0.0.1",
"description": "Platform-specific httptape binary for macOS x64 (Intel)",
"license": "Apache-2.0",
"os": ["darwin"],
"cpu": ["x64"],
"files": ["bin/"],
"preferUnplugged": true
}
1 change: 1 addition & 0 deletions packages/binary-linux-arm64/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Platform-specific httptape binary for Linux ARM64. Do not install directly -- use `vite-plugin-httptape` instead.
Empty file.
10 changes: 10 additions & 0 deletions packages/binary-linux-arm64/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "@httptape/binary-linux-arm64",
"version": "0.0.1",
"description": "Platform-specific httptape binary for Linux ARM64",
"license": "Apache-2.0",
"os": ["linux"],
"cpu": ["arm64"],
"files": ["bin/"],
"preferUnplugged": true
}
1 change: 1 addition & 0 deletions packages/binary-linux-x64/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Platform-specific httptape binary for Linux x64. Do not install directly -- use `vite-plugin-httptape` instead.
Empty file.
10 changes: 10 additions & 0 deletions packages/binary-linux-x64/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "@httptape/binary-linux-x64",
"version": "0.0.1",
"description": "Platform-specific httptape binary for Linux x64",
"license": "Apache-2.0",
"os": ["linux"],
"cpu": ["x64"],
"files": ["bin/"],
"preferUnplugged": true
}
1 change: 1 addition & 0 deletions packages/binary-win32-x64/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Platform-specific httptape binary for Windows x64. Do not install directly -- use `vite-plugin-httptape` instead.
Empty file.
10 changes: 10 additions & 0 deletions packages/binary-win32-x64/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"name": "@httptape/binary-win32-x64",
"version": "0.0.1",
"description": "Platform-specific httptape binary for Windows x64",
"license": "Apache-2.0",
"os": ["win32"],
"cpu": ["x64"],
"files": ["bin/"],
"preferUnplugged": true
}
47 changes: 47 additions & 0 deletions packages/vite-plugin-httptape/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# vite-plugin-httptape

Vite plugin that manages an [httptape](https://github.com/httptape/httptape) process during `vite dev`. Automatically starts httptape when the dev server starts, proxies your API route through it, and stops it when the server shuts down.

## Install

```bash
pnpm add -D vite-plugin-httptape
```

The correct platform-specific binary (`@httptape/binary-*`) is installed automatically as an optional dependency.

## Quickstart

```ts
// vite.config.ts
import { defineConfig } from 'vite';
import httptape from 'vite-plugin-httptape';

export default defineConfig({
plugins: [
httptape({
upstream: 'https://api.example.com',
route: '/api', // default
tapeDir: '.httptape', // default
mode: 'auto', // default -- replay if tapes exist, else record
}),
],
});
```

That is it. Run `vite dev` and requests to `/api/**` are routed through httptape.

## Options

| Option | Type | Default | Description |
|---|---|---|---|
| `mode` | `'record' \| 'replay' \| 'proxy' \| 'auto'` | `'auto'` | Recording/replay mode |
| `upstream` | `string` | -- | Upstream URL (required for `record` and `proxy` modes) |
| `tapeDir` | `string` | `'.httptape'` | Directory for recorded tapes |
| `route` | `string` | `'/api'` | Route prefix to proxy through httptape |
| `port` | `number` | auto | Fixed port for the httptape process |
| `binary` | `string` | auto | Path to the httptape binary |

## License

Apache-2.0
25 changes: 25 additions & 0 deletions packages/vite-plugin-httptape/eslint.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import tseslint from 'typescript-eslint';

export default tseslint.config(
...tseslint.configs.recommended,
{
languageOptions: {
parserOptions: {
projectService: {
allowDefaultProject: ['test/*.test.ts'],
defaultProject: 'tsconfig.lint.json',
},
tsconfigRootDir: import.meta.dirname,
},
},
rules: {
'@typescript-eslint/no-unused-vars': [
'error',
{ argsIgnorePattern: '^_' },
],
},
},
{
ignores: ['dist/', 'eslint.config.js', 'vitest.config.ts'],
},
);
46 changes: 46 additions & 0 deletions packages/vite-plugin-httptape/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
{
"name": "vite-plugin-httptape",
"version": "0.0.1",
"description": "Vite plugin that manages an httptape process during dev",
"license": "Apache-2.0",
"type": "module",
"exports": {
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
}
},
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"files": [
"dist"
],
"scripts": {
"build": "tsc",
"typecheck": "tsc --noEmit",
"lint": "eslint src/ test/",
"test": "vitest run"
},
"peerDependencies": {
"vite": ">=6"
},
"optionalDependencies": {
"@httptape/binary-darwin-arm64": "workspace:*",
"@httptape/binary-darwin-x64": "workspace:*",
"@httptape/binary-linux-x64": "workspace:*",
"@httptape/binary-linux-arm64": "workspace:*",
"@httptape/binary-win32-x64": "workspace:*"
},
"devDependencies": {
"@types/node": "^22.0.0",
"eslint": "^9.0.0",
"prettier": "^3.0.0",
"typescript": "^5.7.0",
"typescript-eslint": "^8.0.0",
"vite": "^6.0.0",
"vitest": "^3.0.0"
},
"engines": {
"node": ">=20"
}
}
35 changes: 35 additions & 0 deletions packages/vite-plugin-httptape/src/binary.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { createRequire } from 'node:module';

/**
* Resolves the path to the httptape binary.
*
* If `explicitPath` is provided, it is returned as-is.
* Otherwise, the binary is resolved from the platform-specific
* `@httptape/binary-<platform>-<arch>` optional dependency.
*
* @throws {Error} If the platform-specific binary package is not installed.
*/
export function resolveBinary(explicitPath?: string): string {
if (explicitPath) {
return explicitPath;
}

const pkg = `@httptape/binary-${process.platform}-${process.arch}`;
const require = createRequire(import.meta.url);

try {
return require.resolve(`${pkg}/bin/httptape`);
} catch {
throw new Error(
`[httptape] Could not find the httptape binary for your platform.\n` +
`\n` +
` Expected package: ${pkg}\n` +
`\n` +
`Troubleshooting:\n` +
` 1. Run "pnpm install" (or "npm install") to install optional dependencies.\n` +
` 2. If you are on an unsupported platform, set the "binary" option to the\n` +
` path of a manually installed httptape binary.\n` +
` 3. See https://httptape.dev/docs/js for more information.\n`,
);
}
}
71 changes: 71 additions & 0 deletions packages/vite-plugin-httptape/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import type { Plugin } from 'vite';
import type { ChildProcess } from 'node:child_process';
import type { HttptapeOptions } from './options.js';
import { resolveOptions } from './options.js';
import { resolveBinary } from './binary.js';
import { pickFreePort } from './port.js';
import { spawnHttptape, killGracefully } from './process.js';

export type { HttptapeOptions };

/**
* Vite plugin that manages an httptape process during `vite dev`.
*
* Automatically starts httptape when the dev server starts and stops it
* when the dev server shuts down. Configures Vite's built-in proxy to
* route the specified path prefix through httptape.
*
* @example
* ```ts
* // vite.config.ts
* import httptape from 'vite-plugin-httptape';
*
* export default defineConfig({
* plugins: [
* httptape({
* upstream: 'https://api.example.com',
* route: '/api',
* }),
* ],
* });
* ```
*/
export default function httptape(options: HttptapeOptions = {}): Plugin {
const opts = resolveOptions(options);
let child: ChildProcess | undefined;
let port: number;

return {
name: 'vite-plugin-httptape',
apply: 'serve', // dev only

async config() {
port = opts.port || (await pickFreePort());
const routePrefix = opts.route;

return {
server: {
proxy: {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug -- route prefix not regex-escaped: routePrefix is user-supplied and may contain regex-special characters (e.g., /api/v1.0 where . matches any character, or /api/[internal] where brackets are character classes). This silently rewrites the wrong paths.

Escape the prefix before constructing the regex:

function escapeRegExp(s: string): string {
  return s.replace(/[.*+?^${}()|[\]\\]/g, '\\\$&');
}

// in config():
rewrite: (path: string) =>
  path.replace(new RegExp(`^${escapeRegExp(routePrefix)}`), ''),

Alternatively, since this is always a prefix match on a fixed string, you could use path.slice(routePrefix.length) when path.startsWith(routePrefix), which avoids the regex entirely and is cleaner:

rewrite: (path: string) =>
  path.startsWith(routePrefix) ? path.slice(routePrefix.length) : path,

The startsWith approach is simpler and sidesteps the escaping problem entirely. Either fix works.

[routePrefix]: {
target: `http://localhost:${port}`,
changeOrigin: true,
rewrite: (path: string) =>
path.startsWith(routePrefix)
? path.slice(routePrefix.length)
: path,
},
},
},
};
},

configureServer(server) {
const binaryPath = resolveBinary(opts.binary);
child = spawnHttptape(binaryPath, opts, port, server.config.logger);

server.httpServer?.on('close', () => {
killGracefully(child);
});
},
};
}
Loading
Loading