├── .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 | unfetch 3 |
4 | npm 5 | gzip size 6 | downloads 7 | travis 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 | --------------------------------------------------------------------------------