├── .editorconfig
├── .github
└── workflows
│ ├── ci.yml
│ └── size.yml
├── .gitignore
├── LICENSE.md
├── README.md
├── package-lock.json
├── package.json
├── packages
└── isomorphic-unfetch
│ ├── browser.js
│ ├── browser.mjs
│ ├── index.d.ts
│ ├── index.js
│ ├── index.mjs
│ ├── package.json
│ └── readme.md
├── polyfill
├── package.json
└── polyfill.mjs
├── src
├── index.d.ts
└── index.mjs
├── test
├── _setup.js
├── index.test.mjs
├── isomorphic.test.js
├── polyfill.test.js
└── typescript.test.ts
└── tsconfig.json
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = tab
5 | end_of_line = lf
6 | charset = utf-8
7 | trim_trailing_whitespace = true
8 | insert_final_newline = true
9 |
10 | [{package.json,.*rc,*.yml,*.md}]
11 | indent_style = space
12 | indent_size = 2
13 |
14 | [*.md]
15 | trim_trailing_whitespace = false
16 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | workflow_dispatch:
5 | pull_request:
6 | branches:
7 | - "**"
8 | push:
9 | branches:
10 | - main
11 |
12 | jobs:
13 | build_test:
14 | name: Build & Test
15 | runs-on: ubuntu-latest
16 | steps:
17 | - uses: actions/checkout@v3
18 | - uses: actions/setup-node@v3
19 | with:
20 | node-version: 18
21 | - name: Cache node modules
22 | uses: actions/cache@v3
23 | env:
24 | cache-name: cache-node-modules
25 | with:
26 | path: ~/.npm
27 | # This uses the same name as the build-action so we can share the caches.
28 | key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
29 | restore-keys: |
30 | ${{ runner.os }}-build-${{ env.cache-name }}-
31 | ${{ runner.os }}-build-
32 | ${{ runner.os }}-
33 | - run: npm ci
34 | - name: test
35 | run: |
36 | npm run build
37 | npm run test
38 |
--------------------------------------------------------------------------------
/.github/workflows/size.yml:
--------------------------------------------------------------------------------
1 | name: Compressed Size
2 |
3 | on: [pull_request]
4 |
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 | steps:
9 | - uses: actions/checkout@v3
10 | - uses: actions/setup-node@v3
11 | with:
12 | node-version: 18
13 | node-version-file: "package.json"
14 | cache: "npm"
15 | cache-dependency-path: "**/package-lock.json"
16 | - uses: preactjs/compressed-size-action@v1
17 | with:
18 | repo-token: "${{ secrets.GITHUB_TOKEN }}"
19 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | /dist
2 | node_modules
3 | npm-debug.log
4 | .DS_Store
5 | /polyfill/index.js
6 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2017 Jason Miller
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 | # unfetch
11 |
12 | > Tiny 500b fetch "barely-polyfill"
13 |
14 | - **Tiny:** about **500 bytes** of [ES3](https://unpkg.com/unfetch) gzipped
15 | - **Minimal:** just `fetch()` with headers and text/json responses
16 | - **Familiar:** a subset of the full API
17 | - **Supported:** supports IE8+ _(assuming `Promise` is polyfilled of course!)_
18 | - **Standalone:** one function, no dependencies
19 | - **Modern:** written in ES2015, transpiled to 500b of old-school JS
20 |
21 | > 🤔 **What's Missing?**
22 | >
23 | > - Uses simple Arrays instead of Iterables, since Arrays _are_ iterables
24 | > - No streaming, just Promisifies existing XMLHttpRequest response bodies
25 | > - Use in Node.JS is handled by [isomorphic-unfetch](https://github.com/developit/unfetch/tree/master/packages/isomorphic-unfetch)
26 |
27 | * * *
28 |
29 | - [Unfetch](#unfetch)
30 | - [Installation](#installation)
31 | - [Usage: As a Polyfill](#usage-as-a-polyfill)
32 | - [Usage: As a Ponyfill](#usage-as-a-ponyfill)
33 | - [Examples & Demos](#examples--demos)
34 | - [API](#api)
35 | - [Caveats](#caveats)
36 | - [Contribute](#contribute)
37 | - [License](#license)
38 |
39 | * * *
40 |
41 | ## Installation
42 |
43 | For use with [node](http://nodejs.org) and [npm](https://npmjs.com):
44 |
45 | ```sh
46 | npm i unfetch
47 | ```
48 |
49 | Otherwise, grab it from [unpkg.com/unfetch](https://unpkg.com/unfetch/).
50 |
51 | * * *
52 |
53 | ## Usage: As a [Polyfill](https://ponyfill.com/#polyfill)
54 |
55 | This automatically "installs" unfetch as `window.fetch()` if it detects Fetch isn't supported:
56 |
57 | ```js
58 | import 'unfetch/polyfill'
59 |
60 | // fetch is now available globally!
61 | fetch('/foo.json')
62 | .then( r => r.json() )
63 | .then( data => console.log(data) )
64 | ```
65 |
66 | This polyfill version is particularly useful for hotlinking from [unpkg](https://unpkg.com):
67 |
68 | ```html
69 |
70 |
74 | ```
75 |
76 | * * *
77 |
78 | ## Usage: As a [Ponyfill](https://github.com/sindresorhus/ponyfill)
79 |
80 | With a module bundler like [rollup](http://rollupjs.org) or [webpack](https://webpack.js.org),
81 | you can import unfetch to use in your code without modifying any globals:
82 |
83 | ```js
84 | // using JS Modules:
85 | import fetch from 'unfetch'
86 |
87 | // or using CommonJS:
88 | const fetch = require('unfetch')
89 |
90 | // usage:
91 | fetch('/foo.json')
92 | .then( r => r.json() )
93 | .then( data => console.log(data) )
94 | ```
95 |
96 | The above will always return `unfetch()`. _(even if `window.fetch` exists!)_
97 |
98 | There's also a UMD bundle available as [unfetch/dist/unfetch.umd.js](https://unpkg.com/unfetch/dist/unfetch.umd.js), which doesn't automatically install itself as `window.fetch`.
99 |
100 | * * *
101 |
102 | ## Examples & Demos
103 |
104 | [**Real Example on JSFiddle**](https://jsfiddle.net/developit/qrh7tLc0/) ➡️
105 |
106 | ```js
107 | // simple GET request:
108 | fetch('/foo')
109 | .then( r => r.text() )
110 | .then( txt => console.log(txt) )
111 |
112 |
113 | // complex POST request with JSON, headers:
114 | fetch('/bear', {
115 | method: 'POST',
116 | headers: {
117 | 'Content-Type': 'application/json'
118 | },
119 | body: JSON.stringify({ hungry: true })
120 | }).then( r => {
121 | open(r.headers.get('location'));
122 | return r.json();
123 | })
124 | ```
125 |
126 | * * *
127 |
128 | ## API
129 | While one of Unfetch's goals is to provide a familiar interface, its API may differ from other `fetch` polyfills/ponyfills.
130 | One of the key differences is that Unfetch focuses on implementing the [`fetch()` API](https://fetch.spec.whatwg.org/#fetch-api), while offering minimal (yet functional) support to the other sections of the [Fetch spec](https://fetch.spec.whatwg.org/), like the [Headers class](https://fetch.spec.whatwg.org/#headers-class) or the [Response class](https://fetch.spec.whatwg.org/#response-class).
131 | Unfetch's API is organized as follows:
132 |
133 | ### `fetch(url: string, options: Object)`
134 | This function is the heart of Unfetch. It will fetch resources from `url` according to the given `options`, returning a Promise that will eventually resolve to the response.
135 |
136 | Unfetch will account for the following properties in `options`:
137 |
138 | * `method`: Indicates the request method to be performed on the
139 | target resource (The most common ones being `GET`, `POST`, `PUT`, `PATCH`, `HEAD`, `OPTIONS` or `DELETE`).
140 | * `headers`: An `Object` containing additional information to be sent with the request, e.g. `{ 'Content-Type': 'application/json' }` to indicate a JSON-typed request body.
141 | * `credentials`: ⚠ Accepts a `"include"` string, which will allow both CORS and same origin requests to work with cookies. As pointed in the ['Caveats' section](#caveats), Unfetch won't send or receive cookies otherwise. The `"same-origin"` value is not supported. ⚠
142 | * `body`: The content to be transmitted in request's body. Common content types include `FormData`, `JSON`, `Blob`, `ArrayBuffer` or plain text.
143 |
144 | ### `response` Methods and Attributes
145 | These methods are used to handle the response accordingly in your Promise chain. Instead of implementing full spec-compliant [Response Class](https://fetch.spec.whatwg.org/#response-class) functionality, Unfetch provides the following methods and attributes:
146 |
147 | #### `response.ok`
148 | Returns `true` if the request received a status in the `OK` range (200-299).
149 |
150 | #### `response.status`
151 | Contains the status code of the response, e.g. `404` for a not found resource, `200` for a success.
152 |
153 | #### `response.statusText`
154 | A message related to the `status` attribute, e.g. `OK` for a status `200`.
155 |
156 | #### `response.clone()`
157 | Will return another `Object` with the same shape and content as `response`.
158 |
159 | #### `response.text()`, `response.json()`, `response.blob()`
160 | Will return the response content as plain text, JSON and `Blob`, respectively.
161 |
162 | #### `response.headers`
163 | Again, Unfetch doesn't implement a full spec-compliant [`Headers Class`](https://fetch.spec.whatwg.org/#headers), emulating some of the Map-like functionality through its own functions:
164 | * `headers.keys`: Returns an `Array` containing the `key` for every header in the response.
165 | * `headers.entries`: Returns an `Array` containing the `[key, value]` pairs for every `Header` in the response.
166 | * `headers.get(key)`: Returns the `value` associated with the given `key`.
167 | * `headers.has(key)`: Returns a `boolean` asserting the existence of a `value` for the given `key` among the response headers.
168 |
169 | ## Caveats
170 |
171 | _Adapted from the GitHub fetch polyfill [**readme**](https://github.com/github/fetch#caveats)._
172 |
173 | The `fetch` specification differs from `jQuery.ajax()` in mainly two ways that
174 | bear keeping in mind:
175 |
176 | * By default, `fetch` **won't send or receive any cookies** from the server,
177 | resulting in unauthenticated requests if the site relies on maintaining a user
178 | session.
179 |
180 | ```javascript
181 | fetch('/users', {
182 | credentials: 'include'
183 | });
184 | ```
185 |
186 | * The Promise returned from `fetch()` **won't reject on HTTP error status**
187 | even if the response is an HTTP 404 or 500. Instead, it will resolve normally,
188 | and it will only reject on network failure or if anything prevented the
189 | request from completing.
190 |
191 | To have `fetch` Promise reject on HTTP error statuses, i.e. on any non-2xx
192 | status, define a custom response handler:
193 |
194 | ```javascript
195 | fetch('/users')
196 | .then(response => {
197 | if (response.ok) {
198 | return response;
199 | }
200 | // convert non-2xx HTTP responses into errors:
201 | const error = new Error(response.statusText);
202 | error.response = response;
203 | return Promise.reject(error);
204 | })
205 | .then(response => response.json())
206 | .then(data => {
207 | console.log(data);
208 | });
209 | ```
210 |
211 | * * *
212 |
213 | ## Contribute
214 |
215 | First off, thanks for taking the time to contribute!
216 | Now, take a moment to be sure your contributions make sense to everyone else.
217 |
218 | ### Reporting Issues
219 |
220 | Found a problem? Want a new feature? First of all see if your issue or idea has [already been reported](../../issues).
221 | If it hasn't, just open a [new clear and descriptive issue](../../issues/new).
222 |
223 | ### Submitting pull requests
224 |
225 | Pull requests are the greatest contributions, so be sure they are focused in scope, and do avoid unrelated commits.
226 |
227 | > 💁 **Remember: size is the #1 priority.**
228 | >
229 | > Every byte counts! PR's can't be merged if they increase the output size much.
230 |
231 | - Fork it!
232 | - Clone your fork: `git clone https://github.com//unfetch`
233 | - Navigate to the newly cloned directory: `cd unfetch`
234 | - Create a new branch for the new feature: `git checkout -b my-new-feature`
235 | - Install the tools necessary for development: `npm install`
236 | - Make your changes.
237 | - `npm run build` to verify your change doesn't increase output size.
238 | - `npm test` to make sure your change doesn't break anything.
239 | - Commit your changes: `git commit -am 'Add some feature'`
240 | - Push to the branch: `git push origin my-new-feature`
241 | - Submit a pull request with full remarks documenting your changes.
242 |
243 | ## License
244 |
245 | [MIT License](LICENSE.md) © [Jason Miller](https://jasonformat.com/)
246 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "unfetch",
3 | "version": "5.0.0",
4 | "description": "Bare minimum fetch polyfill in 500 bytes",
5 | "unpkg": "./polyfill/index.js",
6 | "main": "./dist/unfetch.js",
7 | "module": "./dist/unfetch.mjs",
8 | "jsnext:main": "./dist/unfetch.mjs",
9 | "umd:main": "./dist/unfetch.umd.js",
10 | "scripts": {
11 | "test": "eslint && tsc -p . --noEmit && NODE_OPTIONS=--experimental-vm-modules jest",
12 | "build": "microbundle src/index.mjs -f cjs,esm,umd && microbundle polyfill/polyfill.mjs -o polyfill/index.js -f cjs --no-sourcemap",
13 | "prepare": "npm run -s build",
14 | "release": "cross-var npm run build -s && cross-var git commit -am $npm_package_version && cross-var git tag $npm_package_version && git push && git push --tags && npm publish"
15 | },
16 | "exports": {
17 | ".": {
18 | "import": "./index.mjs",
19 | "default": "./index.js"
20 | },
21 | "./polyfill": {
22 | "default": "./polyfill/index.js"
23 | },
24 | "./package.json": "./package.json",
25 | "./*": "./*"
26 | },
27 | "workspaces": [
28 | "./packages/isomorphic-unfetch"
29 | ],
30 | "repository": "developit/unfetch",
31 | "keywords": [
32 | "fetch",
33 | "polyfill",
34 | "xhr",
35 | "ajax"
36 | ],
37 | "homepage": "https://github.com/developit/unfetch",
38 | "authors": [
39 | "Jason Miller "
40 | ],
41 | "license": "MIT",
42 | "types": "src/index.d.ts",
43 | "files": [
44 | "src",
45 | "dist",
46 | "polyfill"
47 | ],
48 | "eslintConfig": {
49 | "extends": "developit"
50 | },
51 | "jest": {
52 | "testEnvironmentOptions": {
53 | "url": "http://localhost/"
54 | },
55 | "testMatch": [
56 | "/test/**/*.test.?(m)[jt]s?(x)"
57 | ],
58 | "setupFiles": [
59 | "/test/_setup.js"
60 | ],
61 | "moduleFileExtensions": [
62 | "mjs",
63 | "js",
64 | "ts"
65 | ],
66 | "transform": {
67 | "^.+\\.m?[jt]sx?$": "babel-jest"
68 | }
69 | },
70 | "babel": {
71 | "env": {
72 | "test": {
73 | "presets": [
74 | "@babel/preset-env",
75 | "@babel/preset-typescript"
76 | ]
77 | }
78 | }
79 | },
80 | "devDependencies": {
81 | "@babel/core": "^7.20.7",
82 | "@babel/preset-env": "^7.20.2",
83 | "@babel/preset-typescript": "^7.18.6",
84 | "@types/jest": "^29.2.5",
85 | "@types/node": "^18.11.18",
86 | "cross-var": "^1.1.0",
87 | "eslint": "^7.32.0",
88 | "eslint-config-developit": "^1.2.0",
89 | "jest": "^29.3.1",
90 | "microbundle": "^0.15.1",
91 | "typescript": "^4.9.4"
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/packages/isomorphic-unfetch/browser.js:
--------------------------------------------------------------------------------
1 | module.exports = self.fetch || (self.fetch = require('unfetch').default || require('unfetch'));
2 |
--------------------------------------------------------------------------------
/packages/isomorphic-unfetch/browser.mjs:
--------------------------------------------------------------------------------
1 | import fetch from 'unfetch';
2 | export default self.fetch || (self.fetch = fetch);
3 |
--------------------------------------------------------------------------------
/packages/isomorphic-unfetch/index.d.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Body as NodeBody,
3 | Headers as NodeHeaders,
4 | Request as NodeRequest,
5 | Response as NodeResponse,
6 | RequestInit as NodeRequestInit
7 | } from "node-fetch";
8 |
9 | declare namespace unfetch {
10 | export type IsomorphicHeaders = Headers | NodeHeaders;
11 | export type IsomorphicBody = Body | NodeBody;
12 | export type IsomorphicResponse = Response | NodeResponse;
13 | export type IsomorphicRequest = Request | NodeRequest;
14 | export type IsomorphicRequestInit = RequestInit | NodeRequestInit;
15 | }
16 |
17 | declare const unfetch: typeof fetch;
18 |
19 | export default unfetch;
20 |
--------------------------------------------------------------------------------
/packages/isomorphic-unfetch/index.js:
--------------------------------------------------------------------------------
1 | function r(m) {
2 | return (m && m.default) || m;
3 | }
4 | module.exports = global.fetch =
5 | global.fetch ||
6 | (typeof process == "undefined"
7 | ? r(require("unfetch"))
8 | : function (url, opts) {
9 | if (typeof url === "string" || url instanceof URL) {
10 | url = String(url).replace(/^\/\//g, "https://");
11 | }
12 | return import("node-fetch").then((m) => r(m)(url, opts));
13 | });
14 |
--------------------------------------------------------------------------------
/packages/isomorphic-unfetch/index.mjs:
--------------------------------------------------------------------------------
1 | function r(m) {
2 | return (m && m.default) || m;
3 | }
4 | export default global.fetch =
5 | global.fetch ||
6 | (typeof process == "undefined"
7 | ? function (url, opts) {
8 | return import("unfetch").then((m) => r(m)(url, opts));
9 | }
10 | : function (url, opts) {
11 | if (typeof url === "string" || url instanceof URL) {
12 | url = String(url).replace(/^\/\//g, "https://");
13 | }
14 | return import("node-fetch").then((m) => r(m)(url, opts));
15 | });
16 |
--------------------------------------------------------------------------------
/packages/isomorphic-unfetch/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "isomorphic-unfetch",
3 | "version": "4.0.1",
4 | "description": "Switches between unfetch & node-fetch for client & server.",
5 | "exports": {
6 | ".": {
7 | "import": "./index.mjs",
8 | "default": "./index.js"
9 | },
10 | "./browser": {
11 | "import": "./browser.mjs",
12 | "default": "./browser.js"
13 | },
14 | "./package.json": "./package.json"
15 | },
16 | "license": "MIT",
17 | "repository": "developit/unfetch",
18 | "browser": "browser.js",
19 | "main": "index.js",
20 | "types": "index.d.ts",
21 | "dependencies": {
22 | "node-fetch": "^3.2.0",
23 | "unfetch": "^5.0.0"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/packages/isomorphic-unfetch/readme.md:
--------------------------------------------------------------------------------
1 | # Isomorphic Unfetch
2 |
3 | Switches between [unfetch](https://github.com/developit/unfetch) & [node-fetch](https://github.com/bitinn/node-fetch) for client & server.
4 |
5 | ## Install
6 |
7 | This project uses [node](http://nodejs.org) and [npm](https://npmjs.com). Go check them out if you don't have them locally installed.
8 |
9 | > **Note:** This module uses node-fetch 3.x, which is ES Module and requires Node >= 12.20.0.
10 |
11 | ```sh
12 | $ npm i isomorphic-unfetch
13 | ```
14 |
15 | Then with a module bundler like [rollup](http://rollupjs.org/) or [webpack](https://webpack.js.org/), use as you would anything else:
16 |
17 | ```javascript
18 | // using ES6 modules
19 | import fetch from "isomorphic-unfetch";
20 |
21 | // using CommonJS modules
22 | const fetch = require("isomorphic-unfetch");
23 | ```
24 |
25 | ## Usage
26 |
27 | As a [**ponyfill**](https://ponyfill.com):
28 |
29 | ```js
30 | import fetch from "isomorphic-unfetch";
31 |
32 | fetch("/foo.json")
33 | .then((r) => r.json())
34 | .then((data) => {
35 | console.log(data);
36 | });
37 | ```
38 |
39 | Globally, as a [**polyfill**](https://ponyfill.com/#polyfill):
40 |
41 | ```js
42 | import "isomorphic-unfetch";
43 |
44 | // "fetch" is now installed globally if it wasn't already available
45 |
46 | fetch("/foo.json")
47 | .then((r) => r.json())
48 | .then((data) => {
49 | console.log(data);
50 | });
51 | ```
52 |
53 | ## License
54 |
55 | [MIT License](LICENSE.md) © [Jason Miller](https://jasonformat.com/)
56 |
--------------------------------------------------------------------------------
/polyfill/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "unfetch-polyfill",
3 | "source": "polyfill.mjs",
4 | "main": "index.js"
5 | }
6 |
--------------------------------------------------------------------------------
/polyfill/polyfill.mjs:
--------------------------------------------------------------------------------
1 | import unfetch from '..';
2 | if (!self.fetch) self.fetch = unfetch;
3 |
--------------------------------------------------------------------------------
/src/index.d.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Body as NodeBody,
3 | Headers as NodeHeaders,
4 | Request as NodeRequest,
5 | Response as NodeResponse,
6 | RequestInit as NodeRequestInit,
7 | } from "node-fetch";
8 |
9 | /** @augments Headers */
10 | export interface UnfetchHeaders {
11 | keys: () => string[];
12 | entries: () => [string, string][];
13 | get: (key: string) => string | null;
14 | has: (key: string) => boolean;
15 |
16 | /** @deprecated not supported by unfetch */
17 | append: never;
18 | /** @deprecated not supported by unfetch */
19 | delete: never;
20 | /** @deprecated not supported by unfetch */
21 | forEach: never;
22 | /** @deprecated not supported by unfetch */
23 | set: never;
24 | /** @deprecated not supported by unfetch */
25 | values: never;
26 | /** @deprecated not supported by unfetch */
27 | [Symbol.iterator]: never;
28 | }
29 |
30 | /** @augments Response */
31 | export interface UnfetchResponse {
32 | ok: boolean;
33 | statusText: string;
34 | status: number;
35 | url: string;
36 | text: () => Promise;
37 | json: () => Promise;
38 | blob: () => Promise;
39 | clone: () => UnfetchResponse;
40 | headers: UnfetchHeaders;
41 |
42 | /** @deprecated not supported by unfetch */
43 | arrayBuffer: never;
44 | /** @deprecated not supported by unfetch */
45 | body: never;
46 | /** @deprecated not supported by unfetch */
47 | bodyUsed: never;
48 | /** @deprecated not supported by unfetch */
49 | formData: never;
50 | /** @deprecated not supported by unfetch */
51 | redirected: never;
52 | /** @deprecated not supported by unfetch */
53 | type: never;
54 | }
55 |
56 | /** @augments RequestInit */
57 | export interface UnfetchRequestInit {
58 | method?: string;
59 | headers?: Record;
60 | credentials?: "include" | "omit";
61 | body?: Parameters[0];
62 |
63 | /** @deprecated not supported by unfetch */
64 | cache?: never;
65 | /** @deprecated not supported by unfetch */
66 | integrity?: never;
67 | /** @deprecated not supported by unfetch */
68 | keepalive?: never;
69 | /** @deprecated not supported by unfetch */
70 | mode?: never;
71 | /** @deprecated not supported by unfetch */
72 | redirect?: never;
73 | /** @deprecated not supported by unfetch */
74 | referrer?: never;
75 | /** @deprecated not supported by unfetch */
76 | referrerPolicy?: never;
77 | /** @deprecated not supported by unfetch */
78 | signal?: never;
79 | /** @deprecated not supported by unfetch */
80 | window?: never;
81 | }
82 |
83 | export namespace Unfetch {
84 | export type IsomorphicHeaders = Headers | NodeHeaders;
85 | export type IsomorphicBody = Body | NodeBody;
86 | export type IsomorphicResponse = Response | NodeResponse;
87 | export type IsomorphicRequest = Request | NodeRequest;
88 | export type IsomorphicRequestInit = RequestInit | NodeRequestInit;
89 |
90 | export type Headers = UnfetchHeaders | globalThis.Headers;
91 | export type Body = globalThis.Body;
92 | export type Response = UnfetchResponse | globalThis.Response;
93 | export type Request = UnfetchRequestInit | globalThis.Request;
94 | export type RequestInit = UnfetchRequestInit | globalThis.RequestInit;
95 | }
96 |
97 | export interface Unfetch {
98 | (url: string | URL, options?: UnfetchRequestInit): Promise;
99 | }
100 |
101 | declare const unfetch: Unfetch;
102 |
103 | export default unfetch;
104 |
--------------------------------------------------------------------------------
/src/index.mjs:
--------------------------------------------------------------------------------
1 | export default function (url, options) {
2 | options = options || {};
3 | return new Promise((resolve, reject) => {
4 | const request = new XMLHttpRequest();
5 | const keys = [];
6 | const headers = {};
7 |
8 | const response = () => ({
9 | ok: ((request.status / 100) | 0) == 2, // 200-299
10 | statusText: request.statusText,
11 | status: request.status,
12 | url: request.responseURL,
13 | text: () => Promise.resolve(request.responseText),
14 | json: () => Promise.resolve(request.responseText).then(JSON.parse),
15 | blob: () => Promise.resolve(new Blob([request.response])),
16 | clone: response,
17 | headers: {
18 | keys: () => keys,
19 | entries: () => keys.map((n) => [n, request.getResponseHeader(n)]),
20 | get: (n) => request.getResponseHeader(n),
21 | has: (n) => request.getResponseHeader(n) != null,
22 | },
23 | });
24 |
25 | request.open(options.method || "get", url, true);
26 |
27 | request.onload = () => {
28 | request
29 | .getAllResponseHeaders()
30 | .toLowerCase()
31 | .replace(/^(.+?):/gm, (m, key) => {
32 | headers[key] || keys.push((headers[key] = key));
33 | });
34 | resolve(response());
35 | };
36 |
37 | request.onerror = reject;
38 |
39 | request.withCredentials = options.credentials == "include";
40 |
41 | for (const i in options.headers) {
42 | request.setRequestHeader(i, options.headers[i]);
43 | }
44 |
45 | request.send(options.body || null);
46 | });
47 | }
48 |
--------------------------------------------------------------------------------
/test/_setup.js:
--------------------------------------------------------------------------------
1 | import { jest } from "@jest/globals";
2 |
3 | global.jest = jest;
4 |
--------------------------------------------------------------------------------
/test/index.test.mjs:
--------------------------------------------------------------------------------
1 | import fetch from "../src/index.mjs";
2 | import fetchDist from "..";
3 |
4 | describe("unfetch", () => {
5 | it("should be a function", () => {
6 | expect(fetch).toEqual(expect.any(Function));
7 | });
8 |
9 | it("should be compiled correctly", () => {
10 | expect(fetchDist).toEqual(expect.any(Function));
11 | expect(fetchDist).toHaveLength(2);
12 | });
13 |
14 | describe("fetch()", () => {
15 | let xhr;
16 |
17 | beforeEach(() => {
18 | xhr = {
19 | setRequestHeader: jest.fn(),
20 | getAllResponseHeaders: jest
21 | .fn()
22 | .mockReturnValue(
23 | "X-Foo: bar\r\nX-Foo: baz\r\nX-Bar: bar, baz\r\nx-test: \r\nX-Baz: bar, baz\r\ndate: 18:23:22"
24 | ),
25 | getResponseHeader: jest.fn().mockReturnValue("bar, baz"),
26 | open: jest.fn(),
27 | send: jest.fn(),
28 | status: 200,
29 | statusText: "OK",
30 | responseText: '{"a":"b"}',
31 | responseURL: "/foo?redirect",
32 | };
33 |
34 | global.XMLHttpRequest = jest.fn(() => xhr);
35 | });
36 |
37 | afterEach(() => {
38 | delete global.XMLHttpRequest;
39 | });
40 |
41 | it("sanity test", () => {
42 | let p = fetch("/foo", { headers: { a: "b" } })
43 | .then((r) => {
44 | expect(r).toMatchObject({
45 | text: expect.any(Function),
46 | json: expect.any(Function),
47 | blob: expect.any(Function),
48 | clone: expect.any(Function),
49 | headers: expect.any(Object),
50 | });
51 | expect(r.clone()).not.toBe(r);
52 | expect(r.clone().url).toEqual("/foo?redirect");
53 | expect(r.headers.get).toEqual(expect.any(Function));
54 | expect(r.headers.get("x-foo")).toEqual("bar, baz");
55 | expect(r.headers.keys()).toEqual([
56 | "x-foo",
57 | "x-bar",
58 | "x-test",
59 | "x-baz",
60 | "date",
61 | ]);
62 | return r.json();
63 | })
64 | .then((data) => {
65 | expect(data).toEqual({ a: "b" });
66 |
67 | expect(xhr.setRequestHeader).toHaveBeenCalledTimes(1);
68 | expect(xhr.setRequestHeader).toHaveBeenCalledWith("a", "b");
69 | expect(xhr.open).toHaveBeenCalledTimes(1);
70 | expect(xhr.open).toHaveBeenCalledWith("get", "/foo", true);
71 | expect(xhr.send).toHaveBeenCalledTimes(1);
72 | expect(xhr.send).toHaveBeenCalledWith(null);
73 | });
74 |
75 | expect(xhr.onload).toEqual(expect.any(Function));
76 | expect(xhr.onerror).toEqual(expect.any(Function));
77 |
78 | xhr.onload();
79 |
80 | return p;
81 | });
82 |
83 | it("handles empty header values", () => {
84 | xhr.getAllResponseHeaders = jest
85 | .fn()
86 | .mockReturnValue("Server: \nX-Foo:baz");
87 | const headers = {
88 | server: "",
89 | "x-foo": "baz",
90 | };
91 | xhr.getResponseHeader = jest.fn(
92 | (header) => headers[header.toLowerCase()] ?? null
93 | );
94 | let p = fetch("/foo").then((r) => {
95 | expect(r.headers.get("server")).toEqual("");
96 | expect(r.headers.get("X-foo")).toEqual("baz");
97 | });
98 |
99 | xhr.onload();
100 |
101 | return p;
102 | });
103 | });
104 | });
105 |
--------------------------------------------------------------------------------
/test/isomorphic.test.js:
--------------------------------------------------------------------------------
1 | import fs from "fs";
2 | import vm from "vm";
3 |
4 | describe("isomorphic-unfetch", () => {
5 | describe('"browser" entry', () => {
6 | it("should resolve to fetch when window.fetch exists", () => {
7 | function fetch() {
8 | return this;
9 | }
10 | function unfetch() {}
11 |
12 | let sandbox = {
13 | process: undefined,
14 | window: { fetch },
15 | fetch,
16 | exports: {},
17 | require: () => unfetch,
18 | };
19 | sandbox.global = sandbox.self = sandbox.window;
20 | sandbox.module = { exports: sandbox.exports };
21 | let filename = require.resolve(
22 | "../packages/isomorphic-unfetch/browser.js"
23 | );
24 | vm.runInNewContext(fs.readFileSync(filename, "utf8"), sandbox, filename);
25 |
26 | expect(sandbox.module.exports).toBe(fetch);
27 | });
28 |
29 | it("should resolve to unfetch when window.fetch does not exist", () => {
30 | function unfetch() {}
31 |
32 | let sandbox = {
33 | process: undefined,
34 | window: {},
35 | exports: {},
36 | require: () => unfetch,
37 | };
38 | sandbox.global = sandbox.self = sandbox.window;
39 | sandbox.module = { exports: sandbox.exports };
40 | let filename = require.resolve(
41 | "../packages/isomorphic-unfetch/browser.js"
42 | );
43 | vm.runInNewContext(fs.readFileSync(filename, "utf8"), sandbox, filename);
44 |
45 | expect(sandbox.module.exports).toBe(unfetch);
46 | });
47 | });
48 |
49 | describe('"main" entry', () => {
50 | it("should resolve to fetch when window.fetch exists", () => {
51 | function fetch() {
52 | return this;
53 | }
54 | function unfetch() {}
55 |
56 | let sandbox = {
57 | process: undefined,
58 | window: { fetch },
59 | fetch,
60 | exports: {},
61 | require: () => unfetch,
62 | };
63 | sandbox.global = sandbox.self = sandbox.window;
64 | sandbox.module = { exports: sandbox.exports };
65 | let filename = require.resolve("../packages/isomorphic-unfetch");
66 | vm.runInNewContext(fs.readFileSync(filename, "utf8"), sandbox, filename);
67 |
68 | expect(sandbox.module.exports).toBe(fetch);
69 | });
70 |
71 | it("should resolve to unfetch when window.fetch does not exist", () => {
72 | function unfetch() {}
73 |
74 | let sandbox = {
75 | process: undefined,
76 | window: {},
77 | exports: {},
78 | require: () => unfetch,
79 | };
80 | sandbox.global = sandbox.self = sandbox.window;
81 | sandbox.module = { exports: sandbox.exports };
82 | let filename = require.resolve("../packages/isomorphic-unfetch");
83 | vm.runInNewContext(fs.readFileSync(filename, "utf8"), sandbox, filename);
84 |
85 | expect(sandbox.module.exports).toBe(unfetch);
86 | });
87 | });
88 |
89 | describe('"main" entry in NodeJS', () => {
90 | it("should resolve to fetch when window.fetch exists", () => {
91 | function fetch() {
92 | return this;
93 | }
94 | function unfetch() {}
95 |
96 | let sandbox = {
97 | process: {},
98 | global: { fetch },
99 | exports: {},
100 | require: () => unfetch,
101 | };
102 | sandbox.module = { exports: sandbox.exports };
103 | let filename = require.resolve("../packages/isomorphic-unfetch");
104 | vm.runInNewContext(fs.readFileSync(filename, "utf8"), sandbox, filename);
105 |
106 | expect(sandbox.module.exports).toBe(fetch);
107 | });
108 |
109 | it("should resolve to unfetch when window.fetch does not exist", async () => {
110 | let modules = {
111 | unfetch() {},
112 | "node-fetch": {
113 | default: function nodeFetch(url) {
114 | return "hello from node-fetch";
115 | },
116 | },
117 | };
118 |
119 | let sandbox = {
120 | process: {},
121 | global: {
122 | fetch: null,
123 | },
124 | exports: {},
125 | require: (module) => modules[module],
126 | };
127 | sandbox.global.process = sandbox.process;
128 | sandbox.module = { exports: sandbox.exports };
129 | let filename = require
130 | .resolve("../packages/isomorphic-unfetch")
131 | .replace(/\.js$/, ".mjs");
132 | const context = vm.createContext(sandbox);
133 | const mod = new vm.SourceTextModule(fs.readFileSync(filename, "utf8"), {
134 | context,
135 | async importModuleDynamically(specifier, script, assertions) {
136 | const exp = modules[specifier];
137 | const module = new vm.SyntheticModule(Object.keys(exp), () => {
138 | for (let key in exp) module.setExport(key, exp[key]);
139 | });
140 | await module.link(() => {});
141 | await module.evaluate();
142 | return module;
143 | },
144 | });
145 | await mod.link(() => {});
146 | await mod.evaluate();
147 |
148 | const ns = mod.namespace;
149 |
150 | expect(await ns.default("/")).toBe(
151 | await modules["node-fetch"].default("/")
152 | );
153 | });
154 | });
155 | });
156 |
--------------------------------------------------------------------------------
/test/polyfill.test.js:
--------------------------------------------------------------------------------
1 | import vm from "vm";
2 | import fs from "fs";
3 |
4 | describe("unfetch/polyfill", () => {
5 | it("should resolve to fetch when window.fetch exists", () => {
6 | function fetch() {}
7 | let window = { fetch, require };
8 | window.window = window.global = window.self = window;
9 | let filename = require.resolve("../polyfill/index.js");
10 | vm.runInNewContext(
11 | fs.readFileSync(filename, "utf8"),
12 | window,
13 | "polyfill-test.js"
14 | );
15 | expect(window.fetch).toBe(fetch);
16 | });
17 |
18 | it("should resolve to unfetch when window.fetch does not exist", () => {
19 | let window = { require };
20 | window.window = window.global = window.self = window;
21 | let filename = require.resolve("../polyfill/index.js");
22 | vm.runInNewContext(
23 | fs.readFileSync(filename, "utf8"),
24 | window,
25 | "polyfill-test.js"
26 | );
27 | expect(window.fetch).toEqual(expect.any(Function));
28 | expect(window.fetch).toHaveLength(2);
29 | });
30 | });
31 |
--------------------------------------------------------------------------------
/test/typescript.test.ts:
--------------------------------------------------------------------------------
1 | import unfetch, { Unfetch } from "..";
2 | import isomorphicUnfetch from "../packages/isomorphic-unfetch";
3 |
4 | describe("TypeScript", () => {
5 | describe("browser", () => {
6 | beforeAll(() => {
7 | function XMLHttpRequest() {
8 | const res = {
9 | setRequestHeader: jest.fn(),
10 | getAllResponseHeaders: jest.fn().mockReturnValue(""),
11 | getResponseHeader: jest.fn().mockReturnValue(""),
12 | open: jest.fn((method, url) => {
13 | res.responseURL = url;
14 | }),
15 | send: jest.fn(),
16 | status: 200,
17 | statusText: "OK",
18 | get responseText() {
19 | return this.responseURL.replace(/^data:\,/, "");
20 | },
21 | responseURL: null,
22 | onload: () => {},
23 | };
24 | setTimeout(() => res.onload());
25 | return res;
26 | }
27 |
28 | // @ts-ignore-next-line
29 | global.XMLHttpRequest = jest.fn(XMLHttpRequest);
30 | });
31 |
32 | it("should have valid TypeScript types", async () => {
33 | const res: Unfetch.Response = await unfetch("data:,test");
34 | const text = await res.text();
35 | expect(text).toBe("test");
36 | });
37 |
38 | // This fails because we're abusing Arrays as iterables:
39 | // it("should allow cast to Response", async () => {
40 | // const res: Response = await unfetch("data:,test");
41 | // const r = res.headers.keys()[0]
42 | // });
43 | });
44 |
45 | describe("isomorphic-unfetch", () => {
46 | it("should allow use of standard types like Response", async () => {
47 | const res: Response = await isomorphicUnfetch(new URL("data:,test"));
48 | const blob: Blob = await res.blob();
49 | });
50 |
51 | it("should accept Headers", async () => {
52 | isomorphicUnfetch("data:,test", {
53 | headers: new Headers({ a: "b" }),
54 | mode: "cors",
55 | });
56 | });
57 | });
58 | });
59 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "noEmit": true,
4 | "checkJs": true,
5 | "target": "ESNext",
6 | "moduleResolution": "Node",
7 | "allowSyntheticDefaultImports": true,
8 | "declaration": false,
9 | "declarationDir": null
10 | },
11 | "include": [
12 | "./**/*.ts"
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------