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
71 changes: 71 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Changelog

## 6.0.0

### Breaking Changes

- **WebSocket (WS) and Server-Sent Events (SSE) moved out of core**

- Core no longer supports WS/SSE detection/handling.
- The following legacy behaviors are removed and will now throw a dedicated error:
- `responseType: 'ws'` or `responseType: 'stream'`
- Request URLs starting with `ws://` or `wss://`
Comment thread
dragon-fish marked this conversation as resolved.
Resolved
Comment thread
dragon-fish marked this conversation as resolved.
Resolved
- Responses with `upgrade: websocket` or `content-type: text/event-stream`
- When triggered, core throws `FexiosError` with `code: 'FEATURE_MOVED_TO_PLUGIN'` and a message guiding users to install plugins.

- **`responseType` no longer includes WS/SSE values**

- `FexiosConfigs['responseType']` now only supports:
- `'json' | 'text' | 'form' | 'blob' | 'arrayBuffer'`

- `FexiosPlugin` now should be an object, with a required `name` and `install(fx)` function.

- `name`: a string that uniquely identifies the plugin.
- `install(fx)`: a function that installs the plugin.
- `uninstall(fx)` (optional): a function that will be called when the plugin is uninstalled. You can clean up side effects here.

- **`FexiosContext` is restructured into `ctx.request`, `ctx.runtime`, and `ctx.response`**
- Lifecycle hook context is now a structured object:
- `ctx.request`: request configs and mutable request state (url/query/headers/body/...)
- `ctx.runtime`: runtime-only state (abortController/onProgress/customEnv/...)
- `ctx.response`: parsed `FexiosResponse` (available in `afterResponse` and final context)
- For normal user experience, final context still keeps shortcut getters:
- `ctx.data`, `ctx.headers`, `ctx.url`, `ctx.responseType`, `ctx.rawResponse`, `ctx.rawRequest`

### Migration

- **Use plugins instead of core WS/SSE**

- Install plugins from `fexios/plugins`:
- `pluginWebSocket` for WebSocket
- `pluginSSE` for Server-Sent Events
- New APIs:
- `fx.ws(url, options?) -> Promise<WebSocket>`
- `fx.sse(url, options?) -> Promise<EventSource>`

- **New hook namespaces for real-time features**

- WebSocket hooks:
- `websocket:beforeConnect`, `websocket:open`, `websocket:message`, `websocket:error`, `websocket:close`
- SSE hooks:
- `sse:beforeConnect`, `sse:open`, `sse:message`, `sse:error`, `sse:close`

- **Migrate lifecycle hooks to new context structure**
- Replace request mutations:
- `ctx.url` -> `ctx.request.url`
- `ctx.query` -> `ctx.request.query`
- `ctx.headers` -> `ctx.request.headers`
- Replace runtime env:
- `ctx.customEnv` -> `ctx.runtime.customEnv`
- Parsed response wrapper remains `ctx.response` (type: `FexiosResponse`)

### Added

- **Plugin lifecycle support**

- `FexiosPlugin` now supports optional `uninstall(fx)` for cleanup.
- `fx.plugin(plugin)` now returns an uninstall function (see breaking change above).
- `fx.uninstall(pluginOrName)` is available to remove a plugin at runtime.

- **`ctx.app` in lifecycle hooks**
- Core injects `ctx.app` (current `Fexios` instance) before any lifecycle hooks run, so plugins/users can access app-level info from the context.
19 changes: 7 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Fetch based HTTP client with similar API to axios for browser and Node.js
- [x] 😏 Automatic transforms for JSON data
- [x] 🤩 Instances with custom defaults
- [x] 🫡 Instance extendable
- [x] 😍 Fricking tiny size: `index.umd.cjs 8.51 kB │ gzip: 3.48 kB │ map: 31.96 kB`
- [x] 😍 Fricking tiny size: ~6kb (gzipped)

## Installation

Expand Down Expand Up @@ -347,19 +347,14 @@ fexios.interceptors.response.use((ctx) => {})

## Plugin

```ts
import type { FexiosPlugin } from 'fexios'
See the plugin docs index: [`docs/plugins/README.md`](docs/plugins/README.md)

const authPlugin: FexiosPlugin = (app) => {
app.on('beforeRequest', (ctx) => {
ctx.headers = { ...ctx.headers, Authorization: 'Bearer token' }
return ctx
})
return app // You can return app, or omit the return value
}
Official plugins:

const fx = new Fexios().plugin(authPlugin)
```
- Cookie Jar: [`docs/plugins/cookie-jar.md`](docs/plugins/cookie-jar.md)
- SSE (EventSource): [`docs/plugins/sse.md`](docs/plugins/sse.md)
- WebSocket: [`docs/plugins/websocket.md`](docs/plugins/websocket.md)
- ...and maybe more?

---

Expand Down
18 changes: 6 additions & 12 deletions README.zh_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,7 +233,6 @@ Fexios 采用简化的两阶段合并策略:
- 如果你在钩子中替换了 `ctx.url`,除非你手动保留,否则原 URL 中的 search params 将会丢失。
- 如需在钩子中修改查询参数,建议直接操作 `ctx.query`。


## 钩子

你可以在钩子回调中修改上下文,然后将其作为全新的上下文 ™ 返回。
Expand Down Expand Up @@ -333,19 +332,14 @@ fexios.interceptors.response.use((ctx) => {})

## 插件

```ts
import type { FexiosPlugin } from 'fexios'
插件文档主目录:[`docs/plugins/README.md`](docs/plugins/README.md)

const authPlugin: FexiosPlugin = (app) => {
app.on('beforeRequest', (ctx) => {
ctx.headers = { ...ctx.headers, Authorization: 'Bearer token' }
return ctx
})
return app // 你可以返回 app,或者省略返回值
}
官方插件:

const fx = new Fexios().plugin(authPlugin)
```
- Cookie Jar:[`docs/plugins/cookie-jar.md`](docs/plugins/cookie-jar.md)
- SSE (EventSource):[`docs/plugins/sse.md`](docs/plugins/sse.md)
- WebSocket:[`docs/plugins/websocket.md`](docs/plugins/websocket.md)
- ……也许会有更多?

---

Expand Down
104 changes: 104 additions & 0 deletions docs/plugins/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# Plugins

This directory contains documentation for **official Fexios plugins**.

## Import path

All official plugins are exported from:

```ts
import { pluginXXX } from 'fexios/plugins'
// ↑↑↑↑↑↑↑↑↑ Replace with the real plugin you want to use
```

## Official plugins

- **Cookie Jar**: [`docs/plugins/cookie-jar.md`](cookie-jar.md)
- **SSE (EventSource)**: [`docs/plugins/sse.md`](sse.md)
- **WebSocket**: [`docs/plugins/websocket.md`](websocket.md)

## Creating a plugin (tutorial)

Fexios plugins are **objects** with a required `name` and `install(fx)` function.

### 1) Minimal plugin template

```ts
import { Fexios, type FexiosPlugin } from 'fexios'

export const authPlugin: FexiosPlugin = {
name: 'auth-plugin',
install(fx) {
fx.on('beforeRequest', (ctx) => {
ctx.headers = { ...ctx.headers, Authorization: 'Bearer token' }
return ctx
})
},
}

const fx = new Fexios()
await fx.plugin(authPlugin)
```

### 2) Uninstalling a plugin

If your plugin defines `uninstall(fx)`, Fexios will call it when you uninstall:

```ts
export const demoPlugin: FexiosPlugin = {
name: 'demo-plugin',
install() {
// setup...
},
uninstall() {
// cleanup...
},
}

const fx = new Fexios()
await fx.plugin(demoPlugin)

// uninstall by name or by reference
fx.uninstall('demo-plugin')
// fx.uninstall(demoPlugin)
```

### 3) Adding instance methods (TypeScript module augmentation)

Plugins can attach new methods/fields onto the `Fexios` instance. To make it type-safe,
add module augmentation in your plugin module:

```ts
import type { FexiosPlugin } from 'fexios'

declare module 'fexios' {
interface Fexios {
hello: (name: string) => string
}
}

export const helloPlugin: FexiosPlugin = {
name: 'hello-plugin',
install(fx) {
fx.hello = (name) => `Hello, ${name}`
},
uninstall(fx) {
fx.hello = undefined as any
},
}
```

### 4) Hooks & lifecycle events

Inside `install(fx)`, you can use:

- `fx.on('<lifecycleEvent>', handler)` to intercept request/response lifecycle.
- `fx.interceptors.request.use(...)` / `fx.interceptors.response.use(...)` as axios-like sugar.

Official plugins (SSE/WebSocket) also emit custom lifecycle events such as
`sse:beforeConnect` / `websocket:beforeConnect`.

## Runtime notes (browser vs Node.js)

- In browsers, `EventSource` and `WebSocket` are usually available globally.
- In Node.js, you may need to provide polyfills (set them on `globalThis`) before using `pluginSSE` / `pluginWebSocket`.
57 changes: 57 additions & 0 deletions docs/plugins/cookie-jar.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Cookie Jar Plugin

> [!IMPORTANT]
>
> This plugin is only available in **Node.js** environment.

The Cookie Jar plugin adds **automatic cookie persistence** for Fexios requests:

- It appends a `Cookie` header for outgoing requests (based on request URL).
- It parses `Set-Cookie` from responses and stores cookies in-memory.
- It exposes an instance at `fx.cookieJar` for manual access.

## Import

```ts
import { Fexios } from 'fexios'
import { pluginCookieJar } from 'fexios/plugins'
```

## Usage

```ts
const fx = new Fexios({ baseURL: 'https://example.com' }).plugin(
pluginCookieJar
)

// The plugin exposes the jar:
fx.cookieJar?.setCookie({
name: 'foo',
value: 'bar',
domain: 'example.com',
path: '/',
})

// Requests will automatically include matching cookies
await fx.get('/profile')
```

## API

The plugin exposes a `CookieJar` instance at `fx.cookieJar`.

Common methods:

- `cookieJar.setCookie(cookie, domain?, path?)`
- `cookieJar.getCookie(name, domain?, path?)`
- `cookieJar.getCookies(domain?, path?)`
- `cookieJar.getCookieHeader(domain?, path?)`
- `cookieJar.clear()`
- `cookieJar.cleanExpiredCookies()`

## Notes

- **In-memory only**: cookies are stored in memory; persistence is up to your app.
- **Multiple Set-Cookie headers**:
- If the runtime supports `headers.getSetCookie()` (e.g. undici), the plugin will read all values.
- Otherwise it falls back to `headers.get('set-cookie')`, which may not preserve multiple cookies in some environments.
51 changes: 51 additions & 0 deletions docs/plugins/sse.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# SSE (EventSource) Plugin

This plugin adds `fx.sse(...)` to create an **SSE (Server-Sent Events)** connection using `EventSource`.

## Import

```ts
import { Fexios } from 'fexios'
import { pluginSSE } from 'fexios/plugins'
```

## Usage

```ts
const fx = new Fexios({ baseURL: 'https://example.com' }).plugin(pluginSSE)

const es = await fx.sse('/events', {
query: { room: 'lobby' },
timeout: 30_000,
})

es.addEventListener('message', (event) => {
console.log('message:', event.data)
})
```

## API

### `fx.sse(url, options?)`

- **Returns**: `Promise<EventSource>` (resolves after the connection is opened)
- **options.query**: query params to merge into the URL
- **options.timeout**: connect timeout in ms (default: `fx.baseConfigs.timeout ?? 60000`)

## Lifecycle events

The plugin emits the following Fexios lifecycle events:

- `sse:beforeConnect` — allow you to modify `{ url, timeout }` before connecting
- `sse:open`
- `sse:message`
- `sse:error`

## Runtime notes (Node.js)

In Node.js, `EventSource` is not always available globally. You can provide a polyfill:

```ts
import EventSource from 'eventsource'
;(globalThis as any).EventSource = EventSource
```
Loading