# Technical Documentation: elbywan/wretch

> ℹ️ **Provenance:** Multi-Tier Ingestion · [CodeWiki Reference](https://codewiki.google/github.com/elbywan/wretch)

### Overview (`README.md`)

<h1 align="center">
	<a href="https://elbywan.github.io/wretch"><img src="assets/wretch.svg" alt="wretch-logo" width="220px"></a><br>
	<br>
    <a href="https://elbywan.github.io/wretch">Wretch</a><br>
	<br>
  <a href="https://github.com/elbywan/wretch/actions/workflows/node.yml"><img alt="github-badge" src="https://github.com/elbywan/wretch/actions/workflows/node.yml/badge.svg"></a>
  <a href="https://www.npmjs.com/package/wretch"><img alt="npm-badge" src="https://img.shields.io/npm/v/wretch.svg?colorB=ff733e" height="20"></a>
  <a href="https://www.npmjs.com/package/wretch"><img alt="npm-downloads-badge" src="https://img.shields.io/npm/dm/wretch.svg?colorB=53aabb" height="20"></a>
  <a href="https://coveralls.io/github/elbywan/wretch?branch=master"><img src="https://coveralls.io/repos/github/elbywan/wretch/badge.svg?branch=master" alt="Coverage Status"></a>
  <a href="https://bundlejs.com/?q=wretch#sharing"><img src='https://deno.bundlejs.com/badge?q=wretch'/></a>
  <a href="https://github.com/elbywan/wretch/blob/master/LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="license-badge" height="20"></a>
</h1>
<h4 align="center">
	A tiny (~1.8KB g-zipped) wrapper built around <a href="https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch">fetch</a> with an intuitive syntax.
</h4>
<h5 align="center">
    <i>f[ETCH] [WR]apper</i>
</h6>

<br>

##### Wretch 3.0 is now live 🎉 ! Check out the [Migration Guide](MIGRATION_V2_V3.md) for upgrading from v2, and please have a look at the [releases](https://github.com/elbywan/wretch/releases) and the [changelog](https://github.com/elbywan/wretch/blob/master/CHANGELOG.md) after each update for new features and breaking changes.

##### And if you like the library please consider becoming a [sponsor](https://github.com/sponsors/elbywan) ❤️.

# Features

#### `wretch` is a small wrapper around [fetch](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch) designed to simplify the way to perform network requests and handle responses.

- 🪶 **Small** - core is less than 1.8KB g-zipped
- 💡 **Intuitive** - lean API, handles errors, headers and (de)serialization
- 🧊 **Immutable** - every call creates a cloned instance that can then be reused safely
- 🔌 **Modular** - plug addons to add new features, and middlewares to intercept requests
- 🧩 **Isomorphic** - compatible with modern browsers, Node.js 22+, Deno and Bun
- 🦺 **Type safe** - strongly typed, written in TypeScript
- ✅ **Proven** - fully covered by unit tests and widely used
- 💓 **Maintained** - alive and well for many years

# Table of Contents

- [**Quick Start**](#quick-start)
- [**Motivation**](#motivation)
- [**Installation**](#installation)
- [**Compatibility**](#compatibility)
- [**Usage**](#usage)
- [**Recipes**](#recipes)
- [**Api**](#api-)
- [**Addons**](#addons)
- [**Middlewares**](#middlewares)
- [**Migration Guides**](#migration-guides)
- [**License**](#license)

# Quick Start

```bash
# 1️⃣ Install
npm i wretch
```


```javascript
// 2️⃣ Import and create a reusable API client
import wretch from "wretch"

const api = wretch("https://jsonplaceholder.typicode.com")
  .options({ mode: "cors" })

// 3️⃣ Make requests with automatic JSON handling
const post = await api.get("/posts/1").json()
console.log(post.title)

// 4️⃣ POST with automatic serialization
const created = await api
  .post({ title: "New Post", body: "Content", userId: 1 }, "/posts")
  .json()

// 5️⃣ Handle errors elegantly
await api
  .get("/posts/999")
  .notFound(() => console.log("Post not found!"))
  .json()

// 6️⃣ Different response types
const text = await api.get("/posts/1").text()      // Raw text
const response = await api.get("/posts/1").res()   // Raw Response object
const blob = await api.get("/photos/1").blob()     // Binary data
```

# Motivation

#### Because having to write a second callback to process a response body feels awkward.

Fetch needs a second callback to process the response body.


```javascript
fetch("https://jsonplaceholder.typicode.com/posts/1")
  .then(response => response.json())
  .then(json => {
    //Do stuff with the parsed json
  });
```

Wretch does it for you.


```javascript
// Use .res for the raw response, .text for raw text, .json for json, .blob for a blob …
wretch("https://jsonplaceholder.typicode.com/posts/1")
  .get()
  .json(json => {
    // Do stuff with the parsed json
    return json
  });
```

#### Because manually checking and throwing every request error code is tedious.

Fetch won't reject on HTTP error status.


```javascript
fetch("https://jsonplaceholder.typicode.com/posts/1")
  .then(response => {
    if(!response.ok) {
      if(response.status === 404) throw new Error("Not found")
      else if(response.status === 401) throw new Error("Unauthorized")
      else if(response.status === 418) throw new Error("I'm a teapot !")
      else throw new Error("Other error")
    }
    else {/* … */}
  })
  .then(data => {/* … */})
  .catch(error => { /* … */ })
```

Wretch throws when the response is not successful and contains helper methods to handle common codes.


```javascript
wretch("https://jsonplaceholder.typicode.com/posts/1")
  .get()
  .notFound(error => { /* … */ })
  .unauthorized(error => { /* … */ })
  .error(418, error => { /* … */ })
  .res(response => {/* … */ })
  .catch(error => { /* uncaught errors */ })
```

#### Because sending a json object should be easy.

With fetch you have to set the header, the method and the body manually.


```javascript
fetch("https://jsonplaceholder.typicode.com/posts", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ "hello": "world" })
}).then(response => {/* … */})
// Omitting the data retrieval and error management parts…
```

With wretch, you have shorthands at your disposal.


```javascript
wretch("https://jsonplaceholder.typicode.com/posts")
  .post({ "hello": "world" })
  .res(response => { /* … */ })
```

#### Because configuration should not rhyme with repetition.

A Wretch object is immutable which means that you can reuse previous instances safely.


```javascript
const token = "MY_SECRET_TOKEN"

// Cross origin authenticated requests on an external API
const externalApi = wretch("https://jsonplaceholder.typicode.com") // Base url
  // Authorization header
  .auth(`Bearer ${token}`)
  // Cors fetch options
  .options({ credentials: "include", mode: "cors" })
  // Handle 403 errors
  .resolve((w) => w.forbidden(error => { /* Handle all 403 errors */ }));

// Fetch a resource
const resource = await externalApi
  // Add a custom header for this request
  .headers({ "If-Unmodified-Since": "Wed, 21 Oct 2015 07:28:00 GMT" })
  .get("/posts/1")
  .json(() => {/* do something with the resource */});

// Post a resource
externalApi
  .url("/posts")
  .post({ "Shiny new": "resource object" })
  .json(() => {/* do something with the created resource */});
```

# Installation

## Package Manager

```sh
npm i wretch # or yarn/pnpm add wretch
```

## &lt;script&gt; tag

The package contains multiple bundles depending on the format and feature set located under the `/dist/bundle` folder.

<details>

<summary>Bundle variants</summary>
<br>

> 💡 If you pick the core bundle, then to plug addons you must import them separately from `/dist/bundle/addons/[addonName].min.js`

| Feature set        | File Name           |
| ------------------ | ------------------- |
| Core features only | `wretch.min.js`     |
| Core + all addons  | `wretch.all.min.js` |

| Format   | Extension  |
| -------- | ---------- |
| ESM      | `.min.mjs` |
| CommonJS | `.min.cjs` |
| UMD      | `.min.js`  |

</details>

```html



<script src="https://unpkg.com/wretch"></script>


<script type="module">
  import wretch from 'https://cdn.skypack.dev/wretch/dist/bundle/wretch.all.min.mjs'

  // … //
</script>
```

# Compatibility

## Browsers

`wretch@^3` is compatible with modern browsers only. For older browsers please use `wretch@^1`.

## Node.js

Wretch is compatible with and tested in _Node.js >= 22_.

For older Node.js versions, please use `wretch@^2`.

Node.js 22+ includes native fetch support and all required Web APIs (FormData, URLSearchParams, AbortController, etc.) out of the box, so no polyfills are needed.

## Deno

Works with [Deno](https://deno.land/) out of the box.

```bash
deno add npm:wretch
```


```ts
import wretch from "wretch";

const text = await wretch("https://httpbingo.org").get("/status/200").text();
console.log(text); // -> { "code": 200, "description": "OK" }
```

## Bun

Works with [Bun](https://bun.sh/) out of the box.

```bash
bun add wretch
```


```ts
import wretch from "wretch";

const text = await wretch("https://httpbingo.org").get("/status/200").text();
console.log(text); // -> { "code": 200, "description": "OK" }
```

# Usage

## Import


```typescript
// ECMAScript modules
import wretch from "wretch"
// CommonJS
const wretch = require("wretch")
// Global variable (script tag)
window.wretch
```

## Common Use Cases


```javascript
// 🌐 REST API Client
const api = wretch("https://jsonplaceholder.typicode.com")
  .auth("Bearer token")
  .resolve(r => r.json())

const users = await api.get("/users")
const user = await api.post({ name: "John" }, "/users")
users
```


```javascript
// 📤 File Upload with Progress
import ProgressAddon from "wretch/addons/progress"
import FormDataAddon from "wretch/addons/formData"

await wretch("https://httpbingo.org/post")
  .addon([FormDataAddon, ProgressAddon()])
  .formData({ file: file })
  .post()
  .progress((loaded, total) => console.log(`${(loaded/total*100).toFixed()}%`))
  .json()
```


```javascript
// 🔄 Auto-retry on Network Failure
import { retry } from "wretch/middlewares"

const resilientApi = wretch()
  .middlewares([retry({ maxAttempts: 3, retryOnNetworkError: true })])
```


```typescript
// 🎯 Type-safe TypeScript
interface User { id: number; name: string; email: string }

const user = await wretch("https://jsonplaceholder.typicode.com")
  .get("/users/1")
  .json<User>() // Fully typed!

user
```


```javascript
// 🔐 Automatic Token Refresh
const api = wretch("https://httpbingo.org/basic-auth/user/pass")
  .addon(BasicAuthAddon)
  .resolve(w => w.unauthorized(async (error, req) => {
    const newToken = await refreshToken()
    return req
    .basicAuth("user", "pass")
    .unauthorized(e => {
      console.log("Still unauthorized after token refresh");
      throw e
    })
    .fetch()
    .json()
  }))
```

## Custom Fetch Implementation

You can provide a custom fetch implementation using the `.fetchPolyfill()` method. This is useful for for a variety of use cases including mocking, adding logging, timing, or other custom behavior to all requests made through a wretch instance.


```js
import wretch from "wretch"

// Per-instance custom fetch
const api = wretch("https://jsonplaceholder.typicode.com")
  .fetchPolyfill((url, opts) => {
    console.log('Fetching:', url)
    console.time(url)
    return fetch(url, opts).finally(() => {
      console.timeEnd(url)
    })
  })

await api.get("/posts").json()
```

### [toFetch() - Fetch Adapter 🔗](https://elbywan.github.io/wretch/api/interfaces/index.Wretch#tofetch)

Converts a wretch instance into a fetch-like function, preserving all configuration (middlewares, catchers, headers, etc.). Useful for integrating wretch with libraries that expect a fetch signature.


```js
const myFetch = wretch("https://jsonplaceholder.typicode.com")
  .auth("Bearer token")
  .catcher(401, (err) => console.log("Unauthorized"))
  .toFetch()

// Use like regular fetch
const response = await myFetch("/users", { method: "GET" })
response
```


```js
// Pass to libraries expecting fetch
import { createClient } from "@apollo/client"

const client = createClient({
  fetch: wretch().auth("Bearer token").toFetch()
})
```

## Chaining

**A high level overview of the successive steps that can be chained to perform a request and parse the result.**

```
┌─────────────────────────────────────────────────────────────────┐
│                        Request Chain                            │
│                                                                 │
│  wretch(url) ──> .helper() ──> .body() ──> .httpMethod()        │
│                    ↓              ↓            ↓                │
│                 .headers()     .json()      .get()              │
│                 .auth()        .body()      .post()             │
│                 .options()                  .put()              │
│                                             .delete()           │
└─────────────────────────────────────────────────────────────────┘
                              ↓
                    [ fetch() is called ]
                              ↓
┌─────────────────────────────────────────────────────────────────┐
│                       Response Chain                            │
│                                                                 │
│  .catcher() ──> .responseType() ──> Promise ──> .then()/.catch()│
│      ↓              ↓                                           │
│  .notFound()     .json()                                        │
│  .unauthorized() .text()                                        │
│  .error()        .blob()                                        │
│                  .res()                                         │
└─────────────────────────────────────────────────────────────────┘
```

**Step-by-step breakdown:**


```ts
// First, instantiate wretch
wretch(baseUrl, baseOptions)
```

_The "request" chain starts here._


```ts
  // Optional - A set of helper methods to set the default options, set accept header, change the current url…
  .<helper method(s)>()
  // Optional - Serialize an object to json or FormData formats and sets the body & header field if needed
  .<body type>()
    // Required - Sends the get/put/post/delete/patch request.
  .<http method>()
```

_The "response" chain starts here._

_Fetch is called after the request chain ends and before the response chain starts._<br>
_The request is on the fly and now it is time to chain catchers and finally call a response type handler._


```ts
  // Optional - You can chain error handlers here
  .<catcher(s)>()
  // Required - Specify the data type you need, which will be parsed and handed to you
  .<response type>()
  // >> Ends the response chain.
```

_From this point on, wretch returns a standard Promise._


```ts
  .then(…)
  .catch(…)
```

### Concrete Example

Here's how the chaining works in practice:


```js
await wretch("https://api.example.com")        // Base URL
  .headers({ "X-Api-Key": "secret" })          // Helper method
  .query({ limit: 10 })                        // Helper method
  .json({ name: "Alice", role: "admin" })      // Body type
  .post("/users")                              // HTTP method (starts request)
  .badRequest(err => console.log("Invalid"))   // Catcher
  .unauthorized(err => console.log("No auth")) // Catcher
  .json(user => console.log(user))             // Response type
```

# Recipes

Looking for common patterns and solutions? Check out the **[Recipes Guide](RECIPES.md)** for practical examples covering:

- **Error Handling** - Parsing error response bodies, custom error types, global handlers
- **TypeScript Patterns** - Typing precomposed instances, reusable catchers
- **File Uploads** - Progress tracking, FormData handling
- **Query Strings** - Filtering undefined values
- **Request Control** - Combining timeouts with AbortControllers, aborting on errors
- **Advanced Patterns** - Token refresh & replay, schema validation, async polling

# [API 🔗](https://elbywan.github.io/wretch/api/)

> 💡 The API documentation is now autogenerated and hosted separately, click the links access it.

### [Factory Method 🔗](https://elbywan.github.io/wretch/api/functions/index.default)

The default export is a factory function used to instantiate wretch.


```js
import wretch from "wretch"

const api = wretch("http://domain.com/", { cache: "default" })
```

### [Helper Methods 🔗](https://elbywan.github.io/wretch/api/interfaces/index.Wretch#accept)

Helper Methods are used to configure the request and program actions.

**Available methods:** [`.url()`](https://elbywan.github.io/wretch/api/interfaces/index.Wretch#url) · [`.options()`](https://elbywan.github.io/wretch/api/interfaces/index.Wretch#options) · [`.headers()`](https://elbywan.github.io/wretch/api/interfaces/index.Wretch#headers) · [`.auth()`](https://elbywan.github.io/wretch/api/interfaces/index.Wretch#auth) · [`.accept()`](https://elbywan.github.io/wretch/api/interfaces/index.Wretch#accept) · [`.content()`](https://elbywan.github.io/wretch/api/interfaces/index.Wretch#content) · [`.signal()`](https://elbywan.github.io/wretch/api/interfaces/index.Wretch#signal) · [`.toFetch()`](https://elbywan.github.io/wretch/api/interfaces/index.Wretch#tofetch) · [`.fetchPolyfill()`](https://elbywan.github.io/wretch/api/interfaces/index.Wretch#fetchpolyfill) · [`.catcher()`](https://elbywan.github.io/wretch/api/interfaces/index.Wretch#catcher) · [`.catcherFallback()`](https://elbywan.github.io/wretch/api/interfaces/index.Wretch#catcherfallback) · [`.customError()`](https://elbywan.github.io/wretch/api/interfaces/index.Wretch#customerror) · [`.defer()`](https://elbywan.github.io/wretch/api/interfaces/index.Wretch#defer) · [`.resolve()`](https://elbywan.github.io/wretch/api/interfaces/index.Wretch#resolve) · [`.middlewares()`](https://elbywan.github.io/wretch/api/interfaces/index.Wretch#middlewares) · [`.addon()`](https://elbywan.github.io/wretch/api/interfaces/index.Wretch#addon) · [`.polyfills()`](https://elbywan.github.io/wretch/api/interfaces/index.Wretch#polyfills)


```js
let api = wretch("http://domain.com/")

api = api
  .url("/posts/1")
  .headers({ "Cache-Control": "no-cache" })
  .content("text/html")
```

### [Body Types 🔗](https://elbywan.github.io/wretch/api/interfaces/index.Wretch#body)

Specify a body type if uploading data. Can also be added through the HTTP Method argument.

**Available methods:** [`.body()`](https://elbywan.github.io/wretch/api/interfaces/index.Wretch#body) · [`.json()`](https://elbywan.github.io/wretch/api/interfaces/index.Wretch#json)


```js
let api = wretch("http://domain.com/")

api = api.body("<html><body><div/></body></html>")
api
```

### [HTTP Methods 🔗](https://elbywan.github.io/wretch/api/interfaces/index.Wretch#delete)

Sets the HTTP method and sends the request.

Calling an HTTP method ends the request chain and returns a response chain.
You can pass optional url and body arguments to these methods.

**Available methods:** [`.get()`](https://elbywan.github.io/wretch/api/interfaces/index.Wretch#get) · [`.post()`](https://elbywan.github.io/wretch/api/interfaces/index.Wretch#post) · [`.put()`](https://elbywan.github.io/wretch/api/interfaces/index.Wretch#put) · [`.patch()`](https://elbywan.github.io/wretch/api/interfaces/index.Wretch#patch) · [`.delete()`](https://elbywan.github.io/wretch/api/interfaces/index.Wretch#delete) · [`.head()`](https://elbywan.github.io/wretch/api/interfaces/index.Wretch#head) · [`.opts()`](https://elbywan.github.io/wretch/api/interfaces/index.Wretch#opts)


```js
const api = wretch("http://jsonplaceholder.typicode.com")

// These shorthands:
api.get("/posts");
api.post({ json: "body" }, "/posts");
// Are equivalent to:
api.url("/posts").get();
api.json({ json: "body" }).url("/posts").post();
```

**NOTE:** if the body argument is an `Object` it is assumed that it is a JSON payload and it will have the same behaviour as calling `.json(body)` unless the `Content-Type` header has been set to something else beforehand.

### [Catchers 🔗](https://elbywan.github.io/wretch/api/interfaces/index.WretchResponseChain#badRequest)

Catchers are optional, but if none are provided an error will still be thrown for http error codes and it will be up to you to catch it.

**Available methods:** [`.badRequest()`](https://elbywan.github.io/wretch/api/interfaces/index.WretchResponseChain#badrequest) · [`.unauthorized()`](https://elbywan.github.io/wretch/api/interfaces/index.WretchResponseChain#unauthorized) · [`.forbidden()`](https://elbywan.github.io/wretch/api/interfaces/index.WretchResponseChain#forbidden) · [`.notFound()`](https://elbywan.github.io/wretch/api/interfaces/index.WretchResponseChain#notfound) · [`.timeout()`](https://elbywan.github.io/wretch/api/interfaces/index.WretchResponseChain#timeout) · [`.internalError()`](https://elbywan.github.io/wretch/api/interfaces/index.WretchResponseChain#internalerror) · [`.error()`](https://elbywan.github.io/wretch/api/interfaces/index.WretchResponseChain#error) · [`.fetchError()`](https://elbywan.github.io/wretch/api/interfaces/index.WretchResponseChain#fetcherror)


```js
wretch("http://domain.com/resource")
  .get()
  .badRequest((err) => console.log(err.status))
  .unauthorized((err) => console.log(err.status))
  .forbidden((err) => console.log(err.status))
  .notFound((err) => console.log(err.status))
  .timeout((err) => console.log(err.status))
  .internalError((err) => console.log(err.status))
  .error(418, (err) => console.log(err.status))
  .fetchError((err) => console.log(err))
  .res();
```

The error passed to catchers is enhanced with additional properties.


```ts
type WretchError = Error & {
  status: number;
  response: WretchResponse;
  url: string;
};
```

By default, `error.message` is set to the response body text (or `statusText` if body parsing fails).

#### Request Replay

The original request is passed along the error and can be used in order to
perform an additional request.


```js
await wretch("https://httpbingo.org/basic-auth/user/pass")
  .addon(BasicAuthAddon)
  .basicAuth("user", "wrongpass")
  .get()
  .unauthorized(async (error, req) => {
    // Renew credentials
    const password = await wretch("https://httpbingo.org/base64/decode/cGFzcw==").get().text();
    // Replay the original request with new credentials
    return req
      .basicAuth("user", password)
      .fetch()
      .unauthorized((err) => { throw err })
      .json();
  })
  .json()
  // The promise chain is preserved as expected
  // ".then" will be performed on the result of the original request
  // or the replayed one (if a 401 error was thrown)
  .then(() => { /* … */ });
```

#### Multiple Error Codes

You can catch multiple error codes with a single handler by passing an array:


```js
// Catch multiple codes at once
const api = wretch("https://httpbingo.org").catcher([401, 403], err => {
  console.log("Authentication/authorization error:", err.status)
});

// The catchers will handle all specified codes
await api.get("/bearer").json()
```

### [Response Types 🔗](https://elbywan.github.io/wretch/api/interfaces/index.WretchResponseChain#arrayBuffer)

Setting the final response body type ends the chain and returns a regular promise.

All these methods accept an optional callback, and will return a Promise
resolved with either the return value of the provided callback or the expected
type.

**Available methods:** [`.res()`](https://elbywan.github.io/wretch/api/interfaces/index.WretchResponseChain#res) · [`.json()`](https://elbywan.github.io/wretch/api/interfaces/index.WretchResponseChain#json) · [`.text()`](https://elbywan.github.io/wretch/api/interfaces/index.WretchResponseChain#text) · [`.blob()`](https://elbywan.github.io/wretch/api/interfaces/index.WretchResponseChain#blob) · [`.arrayBuffer()`](https://elbywan.github.io/wretch/api/interfaces/index.WretchResponseChain#arraybuffer) · [`.formData()`](https://elbywan.github.io/wretch/api/interfaces/index.WretchResponseChain#formdata)


```js
const ENDPOINT = "https://jsonplaceholder.typicode.com/posts/1"

// Without a callback
wretch(ENDPOINT)
  .get()
  .json()
  .then(json => {
    /* the json argument is the parsed json of the response body */
  })
// Without a callback using await
const json = await wretch(ENDPOINT).get().json()
// With a callback the value returned is passed to the Promise
wretch(ENDPOINT).get().json(json => "Hello world!").then(console.log) // => Hello world!
```

_If an error is caught by catchers, the response type handler will not be
called._

# Addons

Addons are separate pieces of code that you can import and plug into `wretch` to add new features.


```js
import FormDataAddon from "wretch/addons/formData"
import QueryStringAddon from "wretch/addons/queryString"

// Add both addons
const w = wretch().addon([FormDataAddon, QueryStringAddon])

// Additional features are now available
w.formData({ hello: "world" }).query({ check: true })
```

Typescript should also be fully supported and will provide completions.

https://user-images.githubusercontent.com/3428394/182319457-504a0856-abdd-4c1d-bd04-df5a061e515d.mov

### [QueryString 🔗](https://elbywan.github.io/wretch/api/interfaces/addons_queryString.QueryStringAddon)

Used to construct and append the query string part of the URL from an object.


```js
import QueryStringAddon from "wretch/addons/queryString"

let w = wretch("http://example.com").addon(QueryStringAddon);
// url is http://example.com
w = w.query({ a: 1, b: 2 });
// url is now http://example.com?a=1&b=2
w = w.query({ c: 3, d: [4, 5] });
// url is now http://example.com?a=1&b=2c=3&d=4&d=5
w = w.query("five&six&seven=eight");
// url is now http://example.com?a=1&b=2c=3&d=4&d=5&five&six&seven=eight
w = w.query({ reset: true }, { replace: true });
// url is now  http://example.com?reset=true
```

### [FormData 🔗](https://elbywan.github.io/wretch/api/interfaces/addons_formData.FormDataAddon)

Adds a helper method to serialize a `multipart/form-data` body from an object.


```js
import FormDataAddon from "wretch/addons/formData"

const form = {
  duck: "Muscovy",
  duckProperties: {
    beak: {
      color: "yellow",
    },
    legs: 2,
  },
  ignored: {
    key: 0,
  },
};

// Will append the following keys to the FormData payload:
// "duck", "duckProperties[beak][color]", "duckProperties[legs]"
wretch("https://httpbingo.org/post").addon(FormDataAddon).formData(form, { recursive: ["ignored"] }).post();
```

### [FormUrl 🔗](https://elbywan.github.io/wretch/api/interfaces/addons_formUrl.FormUrlAddon)

Adds a method to serialize a `application/x-www-form-urlencoded` body from an object.


```js
import FormUrlAddon from "wretch/addons/formUrl"

const form = { a: 1, b: { c: 2 } };
const alreadyEncodedForm = "a=1&b=%7B%22c%22%3A2%7D";

// Automatically sets the content-type header to "application/x-www-form-urlencoded"
wretch("https://httpbingo.org/post").addon(FormUrlAddon).formUrl(form).post();
wretch("https://httpbingo.org/post").addon(FormUrlAddon).formUrl(alreadyEncodedForm).post();
```

### [Abort 🔗](https://elbywan.github.io/wretch/api/variables/addons.abortAddon)

Adds the ability to abort requests and set timeouts using AbortController and signals under the hood.


```js
import AbortAddon from "wretch/addons/abort"
```

Use cases :


```js
const [c, w] = wretch("https://httpbingo.org/get")
  .addon(AbortAddon())
  .get()
  .onAbort((_) => console.log("Aborted !"))
  .controller();

w.text((_) => console.log("should never be called"));
c.abort();

// Or :

const controller = new AbortController();

wretch("https://httpbingo.org/get")
  .addon(AbortAddon())
  .signal(controller)
  .get()
  .onAbort((_) => console.log("Aborted !"))
  .text((_) => console.log("should never be called"));

controller.abort();
```


```js
await wretch("https://httpbingo.org/delay/2")
  .addon(AbortAddon())
  .get()
   // 1 second timeout
  .setTimeout(1000)
  .onAbort(_ => {
    console.log("Request timed out")
  })
  .json(_ => {
    console.log("Response received in time")
  })
```

### [BasicAuth 🔗](https://elbywan.github.io/wretch/api/interfaces/addons_basicAuth.BasicAuthAddon)

Adds the ability to set the `Authorization` header for the [basic authentication scheme](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication#basic_authentication_scheme) without the need to manually encode the username/password.

Also, allows using URLs with `wretch` that contain credentials, which would otherwise throw an error.


```js
import BasicAuthAddon from "wretch/addons/basicAuth"

const user = "user"
const pass = "pass"

// Automatically sets the Authorization header to "Basic " + <base64 encoded credentials>
wretch("https://httpbingo.org/get")
  .addon(BasicAuthAddon)
  .basicAuth(user, pass)
  .get()

// Allows using URLs with credentials in them
wretch(`https://${user}:${pass}@httpbingo.org/basic-auth/${user}/${pass}`)
  .addon(BasicAuthAddon)
  .get()
```

### [Progress 🔗](https://elbywan.github.io/wretch/api/interfaces/addons_progress.ProgressResolver)

Adds the ability to monitor progress when downloading or uploading.

_Compatible with all platforms implementing the [TransformStream WebAPI](https://developer.mozilla.org/en-US/docs/Web/API/TransformStream#browser_compatibility)._

**Download progress:**


```js
import ProgressAddon from "wretch/addons/progress"

await wretch("https://httpbingo.org/bytes/5000")
  .addon(ProgressAddon())
  .get()
  // Called with the number of bytes loaded and the total number of bytes to load
  .progress((loaded, total) => {
    console.log(`Download: ${(loaded / total * 100).toFixed(0)}%`)
  })
  .blob()
```

**Upload progress:**


```js
import ProgressAddon from "wretch/addons/progress"
import FormDataAddon from "wretch/addons/formData"

const formData = new FormData()
formData.append('file', file)

wretch("https://httpbingo.org/post")
  .addon([ProgressAddon(), FormDataAddon])
  .onUpload((loaded, total) => {
    console.log(`Upload: ${(loaded / total * 100).toFixed(0)}%`)
  })
  .post(formData)
  .res()
```

> **Note for browsers:** Upload progress requires HTTPS (HTTP/2) in Chrome/Chromium and doesn't work in Firefox due to streaming limitations. Works fully in Node.js.

### [Performance 🔗](https://elbywan.github.io/wretch/api/variables/addons.perfsAddon)

Adds the ability to measure requests using the Performance Timings API.

Uses the Performance API (browsers & Node.js) to expose timings related to the underlying request.

> 💡 Make sure to follow the additional instructions in the documentation to setup Node.js if necessary.


# Middlewares

Middlewares are functions that can intercept requests before being processed by
Fetch. Wretch includes a helper to help replicate the
[middleware](http://expressjs.com/en/guide/using-middleware.html) style.


```js
import wretch from "wretch"
import { retry, dedupe } from "wretch/middlewares"

const w = wretch().middlewares([retry(), dedupe()])
```

> 💡 The following middlewares were previously provided by the [`wretch-middlewares`](https://github.com/elbywan/wretch-middlewares/) package.

### [Retry 🔗](https://elbywan.github.io/wretch/api/types/middlewares_retry.RetryMiddleware)

**Retries a request multiple times in case of an error (or until a custom condition is true).**

> **💡 By default, the request will be retried only for server errors (5xx) and other non-successful responses, but not for client errors (4xx).**
>
> ```js
> // To retry on all non-2xx responses (including 4xx):
> until: (response, error) => !!response && response.ok
> ```


```js
import wretch from 'wretch'
import { retry } from 'wretch/middlewares'

wretch().middlewares([
  retry({
    /* Options - defaults below */
    delayTimer: 500,
    delayRamp: (delay, nbOfAttempts) => delay * nbOfAttempts,
    maxAttempts: 10,
    until: (response, error) => !!response && (response.ok || (response.status >= 400 && response.status < 500)),
    onRetry: undefined,
    retryOnNetworkError: false,
    resolveWithLatestResponse: false
  })
])

// You can also return a Promise, which is useful if you want to inspect the body:
wretch().middlewares([
  retry({
    until: response =>
      response?.clone().json().then(body =>
        body.field === 'something'
      ) || false
  })
])
```

### [Dedupe 🔗](https://elbywan.github.io/wretch/api/types/middlewares_dedupe.DedupeMiddleware)

**Prevents having multiple identical requests on the fly at the same time.**


```js
import wretch from 'wretch'
import { dedupe } from 'wretch/middlewares'

wretch().middlewares([
  dedupe({
    /* Options - defaults below */
    skip: (url, opts) => opts.skipDedupe || opts.method !== 'GET',
    key: (url, opts) => opts.method + '@' + url,
    resolver: response => response.clone()
  })
])
```

### [Throttling Cache 🔗](https://elbywan.github.io/wretch/api/types/middlewares_throttlingCache.ThrottlingCacheMiddleware)

**A throttling cache which stores and serves server responses for a certain amount of time.**


```js
import wretch from 'wretch'
import { throttlingCache } from 'wretch/middlewares'

wretch().middlewares([
  throttlingCache({
    /* Options - defaults below */
    throttle: 1000,
    skip: (url, opts) => opts.skipCache || opts.method !== 'GET',
    key: (url, opts) => opts.method + '@' + url,
    clear: (url, opts) => false,
    invalidate: (url, opts) => null,
    condition: response => response.ok,
    flagResponseOnCacheHit: '__cached'
  })
])
```

### [Delay 🔗](https://elbywan.github.io/wretch/api/types/middlewares_delay.DelayMiddleware)

**Delays the request by a specific amount of time.**


```js
import wretch from 'wretch'
import { delay } from 'wretch/middlewares'

wretch().middlewares([
  delay(1000)
])
```

## Writing a Middleware

Basically a Middleware is a function having the following signature :


```ts
// A middleware accepts options and returns a configured version
type Middleware = (options?: { [key: string]: any }) => ConfiguredMiddleware;
// A configured middleware (with options curried)
type ConfiguredMiddleware = (next: FetchLike) => FetchLike;
// A "fetch like" function, accepting an url and fetch options and returning a response promise
type FetchLike = (
  url: string,
  opts: WretchOptions,
) => Promise<WretchResponse>;
```

### Context

If you need to manipulate data within your middleware and expose it for later
consumption, a solution could be to pass a named property to the wretch options
(_suggested name: `context`_).

Your middleware can then take advantage of that by mutating the object
reference.


```js
const contextMiddleware = (next) =>
  (url, opts) => {
    if (opts.context) {
      // Mutate "context"
      opts.context.property = "anything";
    }
    return next(url, opts);
  };

// Provide the reference to a "context" object
const context = {};
const res = await wretch("https://httpbingo.org/get")
  // Pass "context" by reference as an option
  .options({ context })
  .middlewares([contextMiddleware])
  .get()
  .res();

console.log(context.property); // prints "anything"
```

### Advanced examples

<details>
  <summary>&nbsp;<strong>👀 Show me the code</strong></summary>
  <br>


```
/* Detailed source-code truncated for AI context efficiency. */
```

</details>

# Limitations

## [Cloudflare Workers](https://workers.cloudflare.com/)

It seems like using `wretch` in a Cloudflare Worker environment is not possible out of the box, as the Cloudflare `Response` implementation does not implement the [`type`](https://developer.mozilla.org/en-US/docs/Web/API/Response/type) property and throws an error when trying to access it.

#### Please check the issue [#159](https://github.com/elbywan/wretch/issues/159) for more information.

### Workaround

The following middleware should fix the issue (thanks @jimmed 🙇):


```js
wretch().middlewares([
  (next) => async (url, opts) => {
    const response = await next(url, opts);
    try {
      Reflect.get(response, "type", response);
    } catch (error) {
      Object.defineProperty(response, "type", {
        get: () => "default",
      });
    }
    return response;
  },
])
```

## Headers Case Sensitivity

The [`Request`](https://developer.mozilla.org/en-US/docs/Web/API/Request) object from the Fetch API uses the [`Headers`](https://developer.mozilla.org/en-US/docs/Web/API/Headers) class to store headers under the hood.
This class is case-insensitive, meaning that setting both will actually appends the value to the same key:


```js
const headers = new Headers();
headers.append("Accept", "application/json");
headers.append("accept", "application/json");
headers.forEach((value, key) => console.log(key, value));
// prints: accept application/json, application/json
```

When using `wretch`, please be mindful of this limitation and avoid setting the same header multiple times with a different case:


```js
wretch("https://httpbingo.org/post")
  .headers({ "content-type": "application/json" })
  // .json is a shortcut for .headers("Content-Type": "application/json").post().json()
  .json({ foo: "bar" })
  // Wretch stores the headers inside a plain javascript object and will not deduplicate them.
  // Later on when fetch builds the Headers object the content type header will be set twice
  // and its value will be "application/json, application/json".
  // Ultimately this is certainly not what you want.
```

#### Please check the issue [#80](https://github.com/elbywan/wretch/issues/80) for more information.

### Workaround

You can use the following middleware to deduplicate headers (thanks @jimmed 🙇):


```js
export const manipulateHeaders =
  callback => next => (url, { headers, ...opts }) => {
    const nextHeaders = callback(new Headers(headers))
    return next(url, { ...opts, headers: nextHeaders })
  }

export const dedupeHeaders = (dedupeHeaderLogic = {}) => {
  const deduperMap = new Map(
    Object.entries(dedupeHeaderLogic).map(([k, v]) => [k.toLowerCase(), v]),
  )
  const dedupe = key =>
    deduperMap.get(key.toLowerCase()) ?? (values => new Set(values))

  return manipulateHeaders((headers) => {
    Object.entries(headers.raw()).forEach(([key, values]) => {
      const deduped = Array.from(dedupe(key)(values))
      headers.delete(key)
      deduped.forEach((value, index) =>
        headers[index ? 'append' : 'set'](key.toLowerCase(), value),
      )
    })
    return headers
  })
}

// By default, it will deduplicate identical values for a given header. This can be used as follows:
wretch().middlewares([dedupeHeaders()])
// If there is a specific header for which the defaults cause problems, then you can provide a callback to handle deduplication yourself:
wretch().middlewares([
  dedupeHeaders({
    Accept: (values) => values.filter(v => v !== '*/*')
  })
])
```

# Migration Guides

Comprehensive migration guides are available for upgrading between major versions:

- **[Migration Guide: v2 to v3](MIGRATION_V2_V3.md)**
- **[Migration Guide: v1 to v2](MIGRATION_V1_V2.md)**

# License

MIT

---

### Overview (`test/snippets/README.md`)

# Documentation Snippet Testing

Automatically tests code examples from README.md, RECIPES.md, and migration guides to ensure they remain valid.

## Usage

```bash
npm run test:snippets  # Run snippet tests
```

## How It Works

1. Extracts code blocks from markdown files at test runtime
2. Parses directives from HTML comments
3. Executes snippets in sandboxed VM contexts
4. Validates outputs, return values, and errors
5. Reports failures with detailed diagnostics

No generated files are created - everything runs dynamically.

## Directives

Directives are special comments that control how snippets are tested. They support:
- **Multi-line directives**: Span across multiple lines
- **Chained directives**: Multiple directives on a single line
- **Parameters**: Pass arguments to directives

### Basic Directives

#### `snippet:skip`

Skip a snippet entirely. Optionally provide a reason:

```markdown

\```js
wretch("http://localhost:3000").get()
\```
```

#### `snippet:description`

Add a description shown when running tests (useful for understanding what's being tested):

```markdown

\```js
const user = await wretch("https://api.example.com/users/1").get().json()
\```
```

Multi-line descriptions are supported:

```markdown

\```js
// code here
\```
```

#### `snippet:await`

Automatically wrap code in an async IIFE (useful for top-level await):

```markdown

\```js
wretch("https://api.example.com").get().json()
\```
```

#### `snippet:timeout`

Override the default 5000ms timeout:

```markdown

\```js
await longRunningOperation()
\```
```

### Error Directives

#### `snippet:expect-error`

Assert that a snippet throws an error:

```markdown

\```js
throw new Error("This should fail")
\```
```

Or expect a specific error message:

```markdown

\```js
throw new Error("Network error occurred")
\```
```

#### `snippet:expect-no-error`

Explicitly assert that a snippet should not throw (useful for snippets that might be auto-skipped):

```markdown

\```js
console.log("This should succeed")
\```
```

### Output Directives

#### `snippet:expect-output`

Assert that console output contains a specific string. Multiple directives can be chained:

```markdown

\```js
console.log("GET /api/users")
console.log("Status: 200")
\```
```

Or use multiple lines:

```markdown


\```js
console.log("Success: Operation complete")
\```
```

#### `snippet:expect-output-regex`

Match console output against a regular expression:

```markdown

\```js
console.log("Found 42 items")
\```
```

### Return Value Directives

#### `snippet:expect-return`

Assert that the snippet's return value (from the last expression) contains a specific string:

```markdown

\```js
const data = await wretch("https://api.example.com/users/1").get().json()
data
\```
```

The code is automatically wrapped in an async IIFE, and the last expression is returned.

#### `snippet:expect-return-json`

Perform complex assertions on return values:

**Simple inclusion** (default):
```markdown

\```js
{ userId: 123, name: "John" }
\```
```

**Exact equality**:
```markdown

\```js
123
\```
```

**Deep equality**:
```markdown

\```js
{ name: "John", age: 30 }
\```
```

**Regex matching**:
```markdown

\```js
"user-123"
\```
```

**Type checking**:
```markdown

\```js
{ foo: "bar" }
\```
```

**Property existence**:
```markdown

\```js
{ userId: 123, name: "John" }
\```
```

**Array operations**:
```markdown

\```js
[1, 2, 3]
\```


\```js
[1, 2, 3]
\```
```

**JSON path** (for nested values):
```markdown

\```js
{ user: { name: "John", age: 30 } }
\```
```

#### `snippet:return-statement`

Specify a custom variable to return when the snippet ends with a variable assignment. Useful when code ends with `const result = ...`:

```markdown


\```js
const result = wretch("https://api.example.com/users/1").get().json()
\```
```

This automatically adds `return result` after the last statement, making the value available for assertions.

### Chaining Directives

Multiple directives can be combined on one line or across multiple lines:

```markdown

\```js
const result = await wretch("https://api.example.com").get().json()
console.log("Success")
\```
```

Or:

```markdown




\```js
const user = await wretch("https://api.example.com/users/1").get().json()
user
\```
```

## Architecture

The framework is designed as a collection of independent, reusable modules:

```
/* Detailed source-code truncated for AI context efficiency. */
```

### Key Design Principles

1. **Zero coupling**: No dependencies on wretch-specific code
2. **Modular**: Each component has a single responsibility
3. **Extensible**: Plugin system for custom functionality
4. **Type-safe**: Full TypeScript support
5. **Clear errors**: Detailed diagnostics when tests fail
6. **Configurable**: Support for config files and programmatic setup

## Customizing

### Adding Custom Directives

Custom directives are added through plugins. Here's an example:

```typescript
import { SnippetRunner } from './snippets/index.js'
import type { Plugin, ControlDirective } from './snippets/plugin.js'

// Create a custom directive
const myDirective: ControlDirective = {
  type: 'control',
  name: 'my-directive',
  description: 'My custom directive',
  handler: (snippet) => {
    console.log(`Processing custom directive for ${snippet.file}:${snippet.line}`)
    return null // Return null to continue execution, or { skip: true, reason: '...' } to skip
  }
}

// Create a plugin that registers the directive
const myPlugin: Plugin = {
  name: 'my-custom-plugin',
  version: '1.0.0',
  directives: [myDirective],
  hooks: {}
}

// Use the plugin
const runner = new SnippetRunner({
  plugins: [myPlugin]
})

await runner.initialize()
```

### Custom Skip Logic

```typescript
const runner = new SnippetRunner({
  shouldSkip: (snippet) => {
    if (snippet.code.includes('SKIP_ME')) {
      return { skip: true, reason: 'Contains SKIP_ME marker' }
    }
    return { skip: false }
  }
})
```

### Adding Files

Edit the `SNIPPET_FILES` array in `snippets.spec.ts`:

```typescript
const SNIPPET_FILES = [
  "README.md",
  "RECIPES.md",
  "MIGRATION_V2_V3.md",
  "docs/ADVANCED.md", // Add new files here
]
```

## Plugin System

The snippet testing framework includes a powerful plugin system that allows you to extend functionality without modifying core code.

### What are Plugins?

Plugins are self-contained modules that can:
- Add custom directives (like `snippet:benchmark`, `snippet:tag`, etc.)
- Hook into the test lifecycle (before/after execution)
- Modify test behavior and results
- Collect metrics and generate reports
- Transform code or context before execution

### Using Plugins

```typescript
import { SnippetRunner } from './snippets/index.js'
import { benchmarkPlugin, loggingPlugin } from './snippets/examples/index.js'

const runner = new SnippetRunner({
  plugins: [benchmarkPlugin, loggingPlugin]
})

// Initialize plugins
await runner.initialize()

// Run tests with plugins active
const results = await runner.runSnippets(snippets)

// Cleanup plugins
await runner.cleanup()
```

### Example Plugins

The framework includes two example plugins demonstrating different capabilities:

#### 1. Logging Plugin

Logs test execution for debugging and monitoring:

```typescript
import { createLoggingPlugin } from './snippets/examples/logging-plugin.js'

const logger = createLoggingPlugin({
  logStart: true,      // Log when tests start
  logComplete: true,   // Log when tests complete
  logErrors: true,     // Log errors
  mode: 'console'      // 'console' or 'collect'
})

const runner = new SnippetRunner({ plugins: [logger] })
```

#### 2. Benchmark Plugin

Adds a `snippet:benchmark` directive to measure execution time:

```typescript
import { createBenchmarkPlugin } from './snippets/examples/benchmark-plugin.js'

const benchmark = createBenchmarkPlugin({
  threshold: 100,      // Warn if > 100ms
  printPerTest: false, // Print per-test results
  printSummary: true   // Print summary report
})

const runner = new SnippetRunner({ plugins: [benchmark] })
```

Use in markdown:

```markdown



\```typescript
const data = await expensiveOperation()
data
\```
```

### Available Plugin Hooks

Plugins can implement these lifecycle hooks:

- **`onInit()`** - Called before any snippets are processed
- **`onBeforeExtract(filePath, content)`** - Called before extracting from a file
- **`onAfterExtract(filePath, snippets)`** - Called after extraction
- **`onBeforeRun(snippet, context)`** - Called before running a snippet
- **`onAfterRun(snippet, result)`** - Called after execution
- **`onTestComplete(testResult)`** - Called after assertions complete
- **`onComplete(results)`** - Called after all tests finish
- **`onError(error, snippet?)`** - Called when errors occur

### Creating Custom Plugins

```typescript
import type { Plugin, ControlDirective } from './snippets/index.js'

// Create a custom directive
const myDirective: ControlDirective = {
  type: 'control',
  name: 'myDirective',
  handler: (snippet, directive) => {
    // Return null to continue, or { skip: true, reason: '...' } to skip
    return null
  }
}

// Create the plugin
export const myPlugin: Plugin = {
  name: 'my-plugin',
  version: '1.0.0',
  description: 'My custom plugin',

  // Register custom directives
  directives: [myDirective],

  // Lifecycle hooks
  hooks: {
    onInit: async () => {
      console.log('Plugin initialized')
    },

    onBeforeRun: async (snippet, context) => {
      // Modify context if needed
      return undefined
    },

    onComplete: async (results) => {
      console.log(`Completed ${results.length} tests`)
    }
  }
}
```

See [examples/README.md](./examples/README.md) for detailed plugin documentation and examples.

## Configuration Files

You can configure the snippet testing system using configuration files instead of code.

### Supported Config Files

The framework looks for these files (in order):

1. `.snippet-testrc.json`
2. `.snippet-testrc.js`
3. `snippet-test.config.json`
4. `snippet-test.config.js`

### JSON Configuration

```json
{
  "files": ["README.md", "docs/**/*.md"],
  "languages": ["typescript", "javascript"],
  "includeUndirected": false,
  "timeout": 5000,
  "showSuccess": true,
  "showSkipped": false,
  "verbose": false,
  "plugins": ["./my-plugin.js"],
  "globals": {
    "API_URL": "https://api.example.com"
  }
}
```

### JavaScript Configuration

```javascript
// snippet-test.config.js
export default {
  files: ["README.md", "docs/**/*.md"],
  languages: ["typescript", "javascript"],
  timeout: 5000,

  // Load plugins
  plugins: ["./my-plugin.js"],

  // Custom module resolver
  moduleResolver: async (specifier) => {
    if (specifier === 'my-lib') {
      return await import('./lib/index.js')
    }
  },

  // Global variables available in snippets
  globals: {
    API_URL: process.env.API_URL || "https://api.example.com"
  }
}
```

### Loading Configuration

```typescript
import { loadConfig, SnippetRunner } from './snippets/index.js'

// Automatically discover and load config from current directory
const config = await loadConfig()

// Or load from specific path
const config = await loadConfig('/path/to/config.json')

// Use config with runner
const runner = new SnippetRunner(config)
```

### Configuration Options

| Option              | Type                  | Default                        | Description                         |
| ------------------- | --------------------- | ------------------------------ | ----------------------------------- |
| `files`             | `string[]`            | `[]`                           | Glob patterns for files to test     |
| `languages`         | `string[]`            | `['typescript', 'javascript']` | Code block languages to extract     |
| `includeUndirected` | `boolean`             | `false`                        | Include snippets without directives |
| `timeout`           | `number`              | `5000`                         | Default execution timeout (ms)      |
| `showSuccess`       | `boolean`             | `true`                         | Show successful tests               |
| `showSkipped`       | `boolean`             | `false`                        | Show skipped tests                  |
| `verbose`           | `boolean`             | `false`                        | Verbose output                      |
| `plugins`           | `Plugin[]`            | `[]`                           | Plugin instances to load            |
| `globals`           | `Record<string, any>` | `{}`                           | Global variables for snippets       |
| `moduleResolver`    | `Function`            | `undefined`                    | Custom module resolver              |
| `shouldSkip`        | `Function`            | `undefined`                    | Custom skip predicate               |

### Merging Configurations

Configurations can be merged programmatically:

```typescript
import { mergeConfigs } from './snippets/index.js'

const baseConfig = { timeout: 5000, showSuccess: true }
const userConfig = { timeout: 10000, verbose: true }

// Later config takes precedence
const merged = mergeConfigs(baseConfig, userConfig)
// Result: { timeout: 10000, showSuccess: true, verbose: true }
```

## Programmatic Usage

```typescript
import { SnippetExtractor, SnippetRunner } from './snippets/index.js'
import { readFile } from 'fs/promises'

// Extract snippets
const extractor = new SnippetExtractor()
const snippets = await extractor.extractFromFile(
  'README.md',
  (path) => readFile(path, 'utf-8')
)

// Run tests
const runner = new SnippetRunner({
  showSuccess: true,
  verbose: true,
})

const results = await runner.runSnippets(snippets, {
  globals: { myCustomGlobal: 'value' }
})

// Print summary
runner.printSummary(results)

// Handle failures
const failed = results.filter(r => !r.passed && !r.skipped)
if (failed.length > 0) {
  process.exit(1)
}
```

## Test APIs

Uses [JSONPlaceholder](https://jsonplaceholder.typicode.com) and [httpbingo](https://httpbingo.org) for testing live HTTP requests.

## Future Extraction

This framework is designed to be extracted as a standalone npm package:

```
@wretch/snippet-testing
```

To extract:
1. Copy `test/snippets/` directory (excluding `snippets.spec.ts`)
2. Add `package.json` with minimal dependencies (`typescript` only)
3. Update import paths to be package-relative
4. Add integration guide for consumers

Dependencies:
- `typescript` (for transpilation) - could be made optional if only testing JS
- `node:vm`, `node:test` (built-in Node.js modules)

That's it! No other dependencies needed.

---

### Overview (`test/snippets/examples/README.md`)

# Example Plugins

This directory contains example plugins that demonstrate how to extend the snippet testing framework.

## Available Example Plugins

### 1. Logging Plugin (`logging-plugin.ts`)

A plugin that logs test execution events for debugging and monitoring purposes.

**Features:**
- Logs test start, completion, and errors
- Two modes: "console" (prints to console) or "collect" (stores in array)
- Configurable verbosity
- Demonstrates all plugin lifecycle hooks

**Usage:**

```typescript
import { loggingPlugin, createLoggingPlugin } from './snippets/examples/logging-plugin.js'
import { SnippetRunner } from './snippets/index.js'

// Use the default instance
const runner = new SnippetRunner({
  plugins: [loggingPlugin]
})

// Or create a customized instance
const customLogger = createLoggingPlugin({
  logStart: true,
  logComplete: true,
  logErrors: true,
  mode: 'console'
})

const runner2 = new SnippetRunner({
  plugins: [customLogger]
})
```

**Options:**
- `logStart` (boolean): Log when snippets start executing
- `logComplete` (boolean): Log when snippets complete
- `logErrors` (boolean): Log errors
- `mode` ('console' | 'collect'): Output mode

### 2. Benchmark Plugin (`benchmark-plugin.ts`)

A plugin that adds performance measurement capabilities with a custom `snippet:benchmark` directive.

**Features:**
- Adds `snippet:benchmark` directive to measure execution time
- Configurable threshold for slow snippet detection
- Summary report with statistics
- Per-test and summary output options
- Demonstrates custom directive registration

**Usage:**

```typescript
import { benchmarkPlugin, createBenchmarkPlugin } from './snippets/examples/benchmark-plugin.js'
import { SnippetRunner } from './snippets/index.js'

// Use the default instance
const runner = new SnippetRunner({
  plugins: [benchmarkPlugin]
})

// Or create a customized instance
const customBenchmark = createBenchmarkPlugin({
  threshold: 50,        // Warn if execution takes > 50ms
  printPerTest: true,   // Print timing for each test
  printSummary: true,   // Print summary at end
  formatter: (result) => `${result.file} took ${result.duration}ms`
})

const runner2 = new SnippetRunner({
  plugins: [customBenchmark]
})
```

**Directive Usage in Markdown:**

````markdown


```typescript
function expensiveOperation() {
  // This snippet's execution time will be measured
  return 42;
}
```
````

**Options:**
- `threshold` (number): Milliseconds threshold for warnings (default: 100)
- `printPerTest` (boolean): Print results after each test (default: false)
- `printSummary` (boolean): Print summary report at end (default: true)
- `formatter` (function): Custom result formatter

**Output Example:**

```
📊 BENCHMARK SUMMARY
============================================================

Top 5 Slowest Snippets:
⚠️  1. ⏱️  README.md:5 (Complex operation) - 125.42ms
   2. ⏱️  RECIPES.md:12 - 85.23ms
   3. ⏱️  README.md:15 - 45.67ms

Statistics:
  Total snippets: 10
  Total time: 456.78ms
  Average: 45.68ms
  Min: 12.34ms
  Max: 125.42ms
  Exceeded threshold (100ms): 1
============================================================
```

## Creating Your Own Plugin

Plugins are objects that implement the `Plugin` interface. Here's a minimal example:

```typescript
import type { Plugin } from '../plugin.js'

export const myPlugin: Plugin = {
  name: 'my-plugin',
  version: '1.0.0',
  description: 'Does something useful',

  // Optional: Register custom directives
  directives: [
    {
      type: 'control',
      name: 'myDirective',
      description: 'My custom directive',
      handler: (snippet, directive) => {
        // Return null to continue, or { skip: true, reason: '...' } to skip
        return null
      }
    }
  ],

  // Optional: Lifecycle hooks
  hooks: {
    onInit: async () => {
      console.log('Plugin initialized')
    },

    onBeforeRun: async (snippet, context) => {
      // Modify context or return undefined to continue
      return undefined
    },

    onAfterRun: async (snippet, result) => {
      // Modify result or return undefined to keep original
      return undefined
    },

    onTestComplete: async (testResult) => {
      // Modify test result or return undefined
      return undefined
    },

    onComplete: async (results) => {
      console.log(`Completed ${results.length} tests`)
    },

    onError: async (error, snippet) => {
      console.error('Error:', error.message)
    }
  },

  // Optional: Plugin lifecycle
  onLoad: async () => {
    console.log('Plugin loaded')
  },

  onUnload: async () => {
    console.log('Plugin unloaded')
  }
}
```

## Available Plugin Hooks

### Lifecycle Hooks (via `hooks` property)

1. **`onInit()`** - Called before any snippets are processed
2. **`onBeforeExtract(filePath, content)`** - Called before extracting from a file
3. **`onAfterExtract(filePath, snippets)`** - Called after extraction
4. **`onBeforeRun(snippet, context)`** - Called before running a snippet
5. **`onAfterRun(snippet, result)`** - Called after execution
6. **`onTestComplete(testResult)`** - Called after assertions
7. **`onComplete(results)`** - Called after all tests finish
8. **`onError(error, snippet?)`** - Called on errors

### Plugin Lifecycle (direct properties)

- **`onLoad()`** - Called when plugin is registered
- **`onUnload()`** - Called when plugin is unregistered

## Custom Directives

Plugins can register four types of directives:

### 1. Control Directives

Control test flow (skip, etc.):

```typescript
{
  type: 'control',
  name: 'myControl',
  handler: (snippet, directive) => {
    if (shouldSkip(snippet)) {
      return { skip: true, reason: 'Not ready' }
    }
    return null
  }
}
```

### 2. Assertion Directives

Validate execution results:

```typescript
{
  type: 'assertion',
  name: 'expectCustom',
  handler: (executionResult, directive) => {
    const passed = validate(executionResult.returnValue)
    return {
      passed,
      message: passed ? undefined : 'Validation failed',
      actual: executionResult.returnValue,
      expected: directive.args[0]
    }
  }
}
```

### 3. Transform Directives

Modify code before execution:

```typescript
{
  type: 'transform',
  name: 'wrapInTry',
  handler: (snippet, directive) => {
    return {
      code: `try {\n${snippet.code}\n} catch (e) { console.error(e) }`,
      modified: true
    }
  }
}
```

### 4. Config Directives

Modify execution context:

```typescript
{
  type: 'config',
  name: 'withTimeout',
  handler: (snippet, directive, context) => {
    return {
      timeout: parseInt(directive.args[0])
    }
  }
}
```

## Using Plugins in Config Files

You can also load plugins via configuration:

```json
{
  "plugins": [
    "./path/to/my-plugin.js"
  ]
}
```

## Best Practices

1. **State Management**: Store plugin state in closures or class properties
2. **Error Handling**: Always handle errors gracefully in hooks
3. **Performance**: Keep hooks fast to avoid slowing down tests
4. **Return Values**: Return `undefined` from hooks when you don't want to modify data
5. **Directive Names**: Use descriptive, unique names for custom directives
6. **Documentation**: Document your plugin's options and behavior

## Next Steps

- Check the [main README](../README.md) for more documentation
- Read the [plugin system architecture](../plugin.ts)
- Explore directive types in [directives/types.ts](../directives/types.ts)

---

--- METRICS ---
- Files Extracted: 3
- Estimated Token Budget: ~15226 tokens
- Canonical Reference: https://codewiki.google/github.com/elbywan/wretch
