├── .editorconfig ├── .gitattributes ├── .github ├── FUNDING.yml └── workflows │ ├── browser.yml │ ├── deno.yml │ └── node.yml ├── .gitignore ├── .nojekyll ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── assets ├── browserstack-logo.png └── wretch.svg ├── browserstack.json ├── docs ├── api │ ├── .nojekyll │ ├── assets │ │ ├── highlight.css │ │ ├── icons.js │ │ ├── icons.svg │ │ ├── main.js │ │ ├── navigation.js │ │ ├── search.js │ │ └── style.css │ ├── functions │ │ ├── addons_abort.default.html │ │ ├── addons_perfs.default.html │ │ ├── addons_progress.default.html │ │ ├── index.default.html │ │ ├── middlewares_dedupe.dedupe.html │ │ ├── middlewares_delay.delay.html │ │ ├── middlewares_retry.retry.html │ │ └── middlewares_throttlingCache.throttlingCache.html │ ├── index.html │ ├── interfaces │ │ ├── addons_abort.AbortResolver.html │ │ ├── addons_abort.AbortWretch.html │ │ ├── addons_basicAuth.BasicAuthAddon.html │ │ ├── addons_formData.FormDataAddon.html │ │ ├── addons_formUrl.FormUrlAddon.html │ │ ├── addons_perfs.PerfsAddon.html │ │ ├── addons_progress.ProgressResolver.html │ │ ├── addons_queryString.QueryStringAddon.html │ │ ├── index.Wretch.html │ │ ├── index.WretchError.html │ │ └── index.WretchResponseChain.html │ ├── modules │ │ ├── addons.html │ │ ├── addons_abort.html │ │ ├── addons_basicAuth.html │ │ ├── addons_formData.html │ │ ├── addons_formUrl.html │ │ ├── addons_perfs.html │ │ ├── addons_progress.html │ │ ├── addons_queryString.html │ │ ├── index.html │ │ ├── middlewares.html │ │ ├── middlewares_dedupe.html │ │ ├── middlewares_delay.html │ │ ├── middlewares_retry.html │ │ └── middlewares_throttlingCache.html │ ├── types │ │ ├── addons_perfs.PerfCallback.html │ │ ├── index.Config.html │ │ ├── index.ConfiguredMiddleware.html │ │ ├── index.FetchLike.html │ │ ├── index.Middleware.html │ │ ├── index.WretchAddon.html │ │ ├── index.WretchDeferredCallback.html │ │ ├── index.WretchErrorCallback.html │ │ ├── index.WretchOptions.html │ │ ├── index.WretchResponse.html │ │ ├── middlewares_dedupe.DedupeKeyFunction.html │ │ ├── middlewares_dedupe.DedupeMiddleware.html │ │ ├── middlewares_dedupe.DedupeOptions.html │ │ ├── middlewares_dedupe.DedupeResolverFunction.html │ │ ├── middlewares_dedupe.DedupeSkipFunction.html │ │ ├── middlewares_delay.DelayMiddleware.html │ │ ├── middlewares_retry.DelayRampFunction.html │ │ ├── middlewares_retry.OnRetryFunction.html │ │ ├── middlewares_retry.OnRetryFunctionResponse.html │ │ ├── middlewares_retry.RetryMiddleware.html │ │ ├── middlewares_retry.RetryOptions.html │ │ ├── middlewares_retry.SkipFunction.html │ │ ├── middlewares_retry.UntilFunction.html │ │ ├── middlewares_throttlingCache.ThrottlingCacheClearFunction.html │ │ ├── middlewares_throttlingCache.ThrottlingCacheConditionFunction.html │ │ ├── middlewares_throttlingCache.ThrottlingCacheInvalidateFunction.html │ │ ├── middlewares_throttlingCache.ThrottlingCacheKeyFunction.html │ │ ├── middlewares_throttlingCache.ThrottlingCacheMiddleware.html │ │ ├── middlewares_throttlingCache.ThrottlingCacheOptions.html │ │ └── middlewares_throttlingCache.ThrottlingCacheSkipFunction.html │ └── variables │ │ ├── addons_basicAuth.default.html │ │ ├── addons_formData.default.html │ │ ├── addons_formUrl.default.html │ │ └── addons_queryString.default.html ├── assets │ ├── ts-logo.svg │ └── wretch.svg ├── index.css └── index.html ├── eslint.config.js ├── karma.conf.cjs ├── package-lock.json ├── package.json ├── rollup.config.js ├── scripts ├── conventional-changelog-wretch │ ├── index.js │ ├── package.json │ └── templates │ │ ├── commit.hbs │ │ ├── header.hbs │ │ └── template.hbs └── mockServer.js ├── src ├── addons │ ├── abort.ts │ ├── basicAuth.ts │ ├── formData.ts │ ├── formUrl.ts │ ├── index.ts │ ├── perfs.ts │ ├── progress.ts │ └── queryString.ts ├── config.ts ├── constants.ts ├── core.ts ├── index.all.ts ├── index.cts ├── index.ts ├── middleware.ts ├── middlewares │ ├── dedupe.ts │ ├── delay.ts │ ├── index.ts │ ├── retry.ts │ └── throttlingCache.ts ├── resolver.ts ├── types.ts └── utils.ts ├── test ├── assets │ └── duck.jpg ├── browser │ └── wretch.spec.js ├── deno │ └── wretch_test.ts ├── mock.js ├── node │ ├── middlewares │ │ ├── dedupe.spec.ts │ │ ├── delay.spec.ts │ │ ├── mock.ts │ │ ├── retry.spec.ts │ │ └── throttling.spec.ts │ └── wretch.spec.ts ├── resolver.cjs └── tsconfig.json ├── tsconfig.cjs.json └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | end_of_line = lf 8 | insert_final_newline = true 9 | charset = utf-8 10 | indent_style = space 11 | indent_size = 2 12 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | test/* linguist-vendored 2 | scripts/* linguist-vendored 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: elbywan 2 | -------------------------------------------------------------------------------- /.github/workflows/browser.yml: -------------------------------------------------------------------------------- 1 | name: Browser test 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - master 8 | - dev 9 | workflow_dispatch: 10 | 11 | jobs: 12 | browsers-test: 13 | runs-on: macos-latest 14 | steps: 15 | - uses: actions/checkout@v3 16 | - uses: actions/setup-node@v3 17 | with: 18 | cache: "npm" 19 | - name: Install firefox 20 | run: brew install --cask firefox 21 | - name: Install dependencies 22 | run: npm ci 23 | - name: Build 24 | run: npm run build 25 | - name: Run browser tests 26 | env: 27 | FIREFOX_BIN: /Applications/Firefox.app/Contents/MacOS/firefox 28 | run: npm run test:karma 29 | -------------------------------------------------------------------------------- /.github/workflows/deno.yml: -------------------------------------------------------------------------------- 1 | name: Deno tests 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - master 8 | - dev 9 | workflow_dispatch: 10 | 11 | jobs: 12 | deno-tests: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v3 16 | - uses: actions/setup-node@v3 17 | with: 18 | node-version: ${{ matrix.version }} 19 | cache: "npm" 20 | - uses: denoland/setup-deno@v1 21 | with: 22 | deno-version: v1.x 23 | - name: Install dependencies 24 | run: npm ci 25 | - name: Build 26 | run: npm run build 27 | - name: Unit tests 28 | run: npm run test:deno 29 | -------------------------------------------------------------------------------- /.github/workflows/node.yml: -------------------------------------------------------------------------------- 1 | name: Node.JS test and lint 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - master 8 | - dev 9 | workflow_dispatch: 10 | 11 | jobs: 12 | nodejs-test: 13 | runs-on: ubuntu-latest 14 | strategy: 15 | matrix: 16 | # Remember to update README.md and package.json when updating the version matrix 17 | version: [16, 18, 20, 22] 18 | steps: 19 | - uses: actions/checkout@v3 20 | - uses: actions/setup-node@v3 21 | with: 22 | node-version: ${{ matrix.version }} 23 | cache: "npm" 24 | - name: Install dependencies 25 | run: npm ci 26 | - name: Lint 27 | run: npm run lint 28 | - name: Build 29 | run: npm run build 30 | - name: Unit tests 31 | run: npm run test 32 | - name: Coveralls 33 | if: ${{ success() }} 34 | uses: coverallsapp/github-action@master 35 | with: 36 | parallel: true 37 | flag-name: node-${{ matrix.version }} 38 | github-token: ${{ secrets.GITHUB_TOKEN }} 39 | path-to-lcov: "./coverage/lcov.info" 40 | 41 | finish: 42 | needs: nodejs-test 43 | runs-on: ubuntu-latest 44 | steps: 45 | - name: Coveralls Finished 46 | uses: coverallsapp/github-action@master 47 | with: 48 | github-token: ${{ secrets.github_token }} 49 | parallel-finished: true 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # IDE 61 | .vscode 62 | 63 | # Rollup typescript cache 64 | .rpt2_cache 65 | 66 | # Distribution files 67 | /dist 68 | /test/browser/src 69 | 70 | # test files 71 | /test.html 72 | -------------------------------------------------------------------------------- /.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elbywan/wretch/bf1dfe8fe532fadc6b6391fb678a785c3efc9b4b/.nojekyll -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | 🎉 Thank you for wishing to collaborate to the wretch library! 4 | 5 | **Ideas, feedback and help are more than welcome.** 6 | 7 | ## How to 8 | 9 | ### 1 - Clone the project 10 | 11 | ```bash 12 | git clone https://github.com/elbywan/wretch 13 | cd wretch 14 | ``` 15 | 16 | ### 2 - Install dependencies 17 | 18 | ```bash 19 | npm install 20 | ``` 21 | 22 | ### 3 - Change the code as you see fit 23 | 24 | ### 4 - Lint, Build and Test 25 | 26 | ```bash 27 | npm start 28 | # To run browser specs 29 | npm run test:karma # same as the ci 30 | npm run test:karma:watch # in watch mode 31 | ``` 32 | 33 | The code **must** pass the linter and specs. 34 | 35 | If you add a new functionality, please write some tests! 36 | 37 | If a linter rule which is not already set in the `.eslintrc.js` file is bothering you, feel free to propose a change. 38 | 39 | ### 5 - Commit & Pull request 40 | 41 | If the modification is related to an existing issue, please mention the number in the commit message. (for instance: `closes #10`) 42 | 43 | Also in order to generate a nice changelog file, please begin your commit message with an emoji corresponding to the change: 44 | 45 | - :fire: `:fire:` -> breaking change 46 | - :bug: `:bug:` -> bug fix 47 | - :factory: `:factory:` -> new feature 48 | - :art: `:art:` -> code improvement 49 | - :checkered_flag: `:checkered_flag:` -> performance update 50 | - :white_check_mark: `:white_check_mark:` -> test improvement 51 | - :memo: `:memo:` -> documentation update 52 | - :arrow_up: `:arrow_up:` -> package update 53 | 54 | Furthermore, starting the actual message content with an upper case and using the present tense and imperative mood would be great. 55 | 56 | And last but not least, always rebase your branch on top of the origin/master branch! 57 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Julien Elbaz 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 | -------------------------------------------------------------------------------- /assets/browserstack-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elbywan/wretch/bf1dfe8fe532fadc6b6391fb678a785c3efc9b4b/assets/browserstack-logo.png -------------------------------------------------------------------------------- /assets/wretch.svg: -------------------------------------------------------------------------------- 1 | Layer 1 -------------------------------------------------------------------------------- /browserstack.json: -------------------------------------------------------------------------------- 1 | { 2 | "test_path": "test/browser/SpecRunner.html?random=false", 3 | "test_framework": "jasmine2", 4 | "exit_with_fail": true, 5 | "browsers": [ 6 | "firefox_latest", 7 | "chrome_latest", 8 | "safari_latest", 9 | "edge_latest" 10 | ] 11 | } -------------------------------------------------------------------------------- /docs/api/.nojekyll: -------------------------------------------------------------------------------- 1 | TypeDoc added this file to prevent GitHub Pages from using Jekyll. You can turn off this behavior by setting the `githubPages` option to false. -------------------------------------------------------------------------------- /docs/api/assets/highlight.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --light-hl-0: #008000; 3 | --dark-hl-0: #6A9955; 4 | --light-hl-1: #0000FF; 5 | --dark-hl-1: #569CD6; 6 | --light-hl-2: #000000; 7 | --dark-hl-2: #D4D4D4; 8 | --light-hl-3: #0070C1; 9 | --dark-hl-3: #4FC1FF; 10 | --light-hl-4: #795E26; 11 | --dark-hl-4: #DCDCAA; 12 | --light-hl-5: #A31515; 13 | --dark-hl-5: #CE9178; 14 | --light-hl-6: #001080; 15 | --dark-hl-6: #9CDCFE; 16 | --light-hl-7: #098658; 17 | --dark-hl-7: #B5CEA8; 18 | --light-hl-8: #AF00DB; 19 | --dark-hl-8: #C586C0; 20 | --light-hl-9: #000000FF; 21 | --dark-hl-9: #D4D4D4; 22 | --light-hl-10: #267F99; 23 | --dark-hl-10: #4EC9B0; 24 | --light-hl-11: #000000; 25 | --dark-hl-11: #C8C8C8; 26 | --light-code-background: #FFFFFF; 27 | --dark-code-background: #1E1E1E; 28 | } 29 | 30 | @media (prefers-color-scheme: light) { :root { 31 | --hl-0: var(--light-hl-0); 32 | --hl-1: var(--light-hl-1); 33 | --hl-2: var(--light-hl-2); 34 | --hl-3: var(--light-hl-3); 35 | --hl-4: var(--light-hl-4); 36 | --hl-5: var(--light-hl-5); 37 | --hl-6: var(--light-hl-6); 38 | --hl-7: var(--light-hl-7); 39 | --hl-8: var(--light-hl-8); 40 | --hl-9: var(--light-hl-9); 41 | --hl-10: var(--light-hl-10); 42 | --hl-11: var(--light-hl-11); 43 | --code-background: var(--light-code-background); 44 | } } 45 | 46 | @media (prefers-color-scheme: dark) { :root { 47 | --hl-0: var(--dark-hl-0); 48 | --hl-1: var(--dark-hl-1); 49 | --hl-2: var(--dark-hl-2); 50 | --hl-3: var(--dark-hl-3); 51 | --hl-4: var(--dark-hl-4); 52 | --hl-5: var(--dark-hl-5); 53 | --hl-6: var(--dark-hl-6); 54 | --hl-7: var(--dark-hl-7); 55 | --hl-8: var(--dark-hl-8); 56 | --hl-9: var(--dark-hl-9); 57 | --hl-10: var(--dark-hl-10); 58 | --hl-11: var(--dark-hl-11); 59 | --code-background: var(--dark-code-background); 60 | } } 61 | 62 | :root[data-theme='light'] { 63 | --hl-0: var(--light-hl-0); 64 | --hl-1: var(--light-hl-1); 65 | --hl-2: var(--light-hl-2); 66 | --hl-3: var(--light-hl-3); 67 | --hl-4: var(--light-hl-4); 68 | --hl-5: var(--light-hl-5); 69 | --hl-6: var(--light-hl-6); 70 | --hl-7: var(--light-hl-7); 71 | --hl-8: var(--light-hl-8); 72 | --hl-9: var(--light-hl-9); 73 | --hl-10: var(--light-hl-10); 74 | --hl-11: var(--light-hl-11); 75 | --code-background: var(--light-code-background); 76 | } 77 | 78 | :root[data-theme='dark'] { 79 | --hl-0: var(--dark-hl-0); 80 | --hl-1: var(--dark-hl-1); 81 | --hl-2: var(--dark-hl-2); 82 | --hl-3: var(--dark-hl-3); 83 | --hl-4: var(--dark-hl-4); 84 | --hl-5: var(--dark-hl-5); 85 | --hl-6: var(--dark-hl-6); 86 | --hl-7: var(--dark-hl-7); 87 | --hl-8: var(--dark-hl-8); 88 | --hl-9: var(--dark-hl-9); 89 | --hl-10: var(--dark-hl-10); 90 | --hl-11: var(--dark-hl-11); 91 | --code-background: var(--dark-code-background); 92 | } 93 | 94 | .hl-0 { color: var(--hl-0); } 95 | .hl-1 { color: var(--hl-1); } 96 | .hl-2 { color: var(--hl-2); } 97 | .hl-3 { color: var(--hl-3); } 98 | .hl-4 { color: var(--hl-4); } 99 | .hl-5 { color: var(--hl-5); } 100 | .hl-6 { color: var(--hl-6); } 101 | .hl-7 { color: var(--hl-7); } 102 | .hl-8 { color: var(--hl-8); } 103 | .hl-9 { color: var(--hl-9); } 104 | .hl-10 { color: var(--hl-10); } 105 | .hl-11 { color: var(--hl-11); } 106 | pre, code { background: var(--code-background); } 107 | -------------------------------------------------------------------------------- /docs/api/assets/navigation.js: -------------------------------------------------------------------------------- 1 | window.navigationData = "data:application/octet-stream;base64,H4sIAAAAAAAAA6WX227bMAyG38W9zQ5t0w7NXZeuwLAN3doFuyiKQImVRIhiZ7LSNRj67oOt2NGJEjXfJj8/ijRJUY9/M0lfZDbKSJ6XRZUNsi2Rq2yUbcp8x2n1Tv3+diU3PBtka1bk2ehskM1XjOeCFtnosUNcz0oh72lV8mcqgqQTW6q4w9Or4fn74evARP4SVM5XCGAnhHAfScXm1zu5uq5Nw0RHC0FvS7G5IZIgmLY0hJwIjiRqSgj4nYpFhcAZOhAmyqWgVYX71B41BP6xo2L/IAUrloizetQQmNTVgUAaOgg2SygiU/vmNIBd4MvIkEahuELSlUHkFllKR10YdygPDNGSQsjfSWVkq4OnbQoE4k2bf/uMSlZIKhZkbiMNueXg4jI2L4NUJQ4xc7ogO65FvdgVc8nKwsIddCbqcvj65OsdMIedApVHaKC7IR+5pk1S5M9EMDLzntYb/fmZEX3bt2DwrQAVO3DvuKF3VMOiX+AdExv3RPBg2BPB0VG7E80fdM3U9f1DromYiJvhB8bb/IuK1ndvu7Eq3lEbirNWjQnnMzJfH5lyv/XhWqEFfH/14fTiLGlEKCZmRLRTHs7eQYBLILireNLYgm2jftOxw2Ki124jMAGaBpUDeK1yc6Czbbt+7aOT4RbSc8GKnL64WWh+RgUeuAgVJH75KcUnIUp/6eicRhWH3dNqWxYVHa8I838LHWqoQ/BxWSzY0m5phVL/xbpYqXaC5t9YnnP6hwga4pnKGP22juYrWwPI7u8YJ3Y2/IlUgq2W0FGaAMe6oQsqBM2hAatjbS3OQ1NjGLwhxLHvts3cClEPEhyvLd0QsNX0uGEUDjNcN11peG4X7U/UcLmh+W5Lv9D97eEkceSJzwZ6aiitr9ojeMMkTHe+eAR91Ie57cWZnBqPYdjTw5ptk71YRrAHTvaJH8C2CLLvySbt8K4NxL8r7qkUKbXpWiDZbptjfWiWkK9Gn/QNXIsgG98ClhyiJlYkshZ/rkQpJWfFckzmKzrmlKR0V8Qc67Usclab9PDsQSC9fy6eCWc5kfT/3XsZSP9poz5ojPSYVPchW6Q/fC+AhkhPiU0StoZ8TgrJeIIXWw9x8+YSQQA7IUziZI8CKR3EEfVoQnBaHcSRZp4RRNeiR9amStN3/VJbpofr2MS2TngHi/jAP0KARSzCR+7hsW0s4sU2xLnz93bElW4UfwuYlXR8CnjwvpKyngXxLpw2EmRdAkujLwE11DKIB28cFoq9JnsObUUenxvTRoKP3L/SurErrGMSix5cayEPlkEiH3rBRv1gn7Xgegv5sQxQfMRw0eDI0YLtckVOaW/g2obQhjzGtgre3z2K6yl8q3tSbsupJUZ1FG7pdxNj+wpxYhnDPwHSz2GzEs8SehAkH8aFJZ4GuYpEjpGwnCAeC8nu8ZMl9nRIdo2cO6iHRLLzlPkEtr1/mtieg4OgmTBPr0//AFxyWgSYJQAA" -------------------------------------------------------------------------------- /docs/api/modules/addons_abort.html: -------------------------------------------------------------------------------- 1 | addons/abort | wretch

Module addons/abort

Index

Interfaces

Functions

default 4 |
5 | -------------------------------------------------------------------------------- /docs/api/modules/addons_basicAuth.html: -------------------------------------------------------------------------------- 1 | addons/basicAuth | wretch

Module addons/basicAuth

Index

Interfaces

Variables

default 3 |
4 | -------------------------------------------------------------------------------- /docs/api/modules/addons_formData.html: -------------------------------------------------------------------------------- 1 | addons/formData | wretch

Module addons/formData

Index

Interfaces

Variables

default 3 |
4 | -------------------------------------------------------------------------------- /docs/api/modules/addons_formUrl.html: -------------------------------------------------------------------------------- 1 | addons/formUrl | wretch

Module addons/formUrl

Index

Interfaces

Variables

default 3 |
4 | -------------------------------------------------------------------------------- /docs/api/modules/addons_perfs.html: -------------------------------------------------------------------------------- 1 | addons/perfs | wretch

Module addons/perfs

Index

Interfaces

Type Aliases

Functions

default 4 |
5 | -------------------------------------------------------------------------------- /docs/api/modules/addons_progress.html: -------------------------------------------------------------------------------- 1 | addons/progress | wretch

Module addons/progress

Index

Interfaces

Functions

default 3 |
4 | -------------------------------------------------------------------------------- /docs/api/modules/addons_queryString.html: -------------------------------------------------------------------------------- 1 | addons/queryString | wretch

Module addons/queryString

Index

Interfaces

Variables

default 3 |
4 | -------------------------------------------------------------------------------- /docs/api/modules/middlewares_delay.html: -------------------------------------------------------------------------------- 1 | middlewares/delay | wretch

Module middlewares/delay

Index

Type Aliases

Functions

delay 3 |
4 | -------------------------------------------------------------------------------- /docs/api/types/addons_perfs.PerfCallback.html: -------------------------------------------------------------------------------- 1 | PerfCallback | wretch

Type Alias PerfCallback

PerfCallback: ((timing: any) => void)
2 | -------------------------------------------------------------------------------- /docs/api/types/index.ConfiguredMiddleware.html: -------------------------------------------------------------------------------- 1 | ConfiguredMiddleware | wretch

Type Alias ConfiguredMiddleware

ConfiguredMiddleware: ((next: FetchLike) => FetchLike)

A ready to use middleware which is called before the request is sent. 2 | Input is the next middleware in the chain, then url and options. 3 | Output is a promise.

4 |
5 | -------------------------------------------------------------------------------- /docs/api/types/index.FetchLike.html: -------------------------------------------------------------------------------- 1 | FetchLike | wretch

Type Alias FetchLike

FetchLike: ((url: string, opts: WretchOptions) => Promise<WretchResponse>)

Any function having the same shape as fetch().

2 |
3 | -------------------------------------------------------------------------------- /docs/api/types/index.WretchOptions.html: -------------------------------------------------------------------------------- 1 | WretchOptions | wretch

Type Alias WretchOptions

WretchOptions: Record<string, any> & RequestInit

Fetch Request options with additional properties.

2 |
3 | -------------------------------------------------------------------------------- /docs/api/types/index.WretchResponse.html: -------------------------------------------------------------------------------- 1 | WretchResponse | wretch

Type Alias WretchResponse

WretchResponse: Response & {
    [key: string]: any;
}

Fetch Response object with additional properties.

2 |
3 | -------------------------------------------------------------------------------- /docs/api/types/middlewares_dedupe.DedupeKeyFunction.html: -------------------------------------------------------------------------------- 1 | DedupeKeyFunction | wretch

Type Alias DedupeKeyFunction

DedupeKeyFunction: ((url: string, opts: WretchOptions) => string)
2 | -------------------------------------------------------------------------------- /docs/api/types/middlewares_dedupe.DedupeResolverFunction.html: -------------------------------------------------------------------------------- 1 | DedupeResolverFunction | wretch

Type Alias DedupeResolverFunction

DedupeResolverFunction: ((response: Response) => Response)
2 | -------------------------------------------------------------------------------- /docs/api/types/middlewares_dedupe.DedupeSkipFunction.html: -------------------------------------------------------------------------------- 1 | DedupeSkipFunction | wretch

Type Alias DedupeSkipFunction

DedupeSkipFunction: ((url: string, opts: WretchOptions) => boolean)
2 | -------------------------------------------------------------------------------- /docs/api/types/middlewares_retry.DelayRampFunction.html: -------------------------------------------------------------------------------- 1 | DelayRampFunction | wretch

Type Alias DelayRampFunction

DelayRampFunction: ((delay: number, nbOfAttempts: number) => number)
2 | -------------------------------------------------------------------------------- /docs/api/types/middlewares_retry.OnRetryFunctionResponse.html: -------------------------------------------------------------------------------- 1 | OnRetryFunctionResponse | wretch

Type Alias OnRetryFunctionResponse

OnRetryFunctionResponse: {
    options?: WretchOptions;
    url?: string;
} | undefined
2 | -------------------------------------------------------------------------------- /docs/api/types/middlewares_retry.SkipFunction.html: -------------------------------------------------------------------------------- 1 | SkipFunction | wretch
SkipFunction: ((url: string, opts: WretchOptions) => boolean)
2 | -------------------------------------------------------------------------------- /docs/api/types/middlewares_retry.UntilFunction.html: -------------------------------------------------------------------------------- 1 | UntilFunction | wretch

Type Alias UntilFunction

UntilFunction: ((response?: Response, error?: Error) => boolean | Promise<boolean>)
2 | -------------------------------------------------------------------------------- /docs/api/types/middlewares_throttlingCache.ThrottlingCacheClearFunction.html: -------------------------------------------------------------------------------- 1 | ThrottlingCacheClearFunction | wretch
ThrottlingCacheClearFunction: ((url: string, opts: WretchOptions) => boolean)
2 | -------------------------------------------------------------------------------- /docs/api/types/middlewares_throttlingCache.ThrottlingCacheConditionFunction.html: -------------------------------------------------------------------------------- 1 | ThrottlingCacheConditionFunction | wretch
ThrottlingCacheConditionFunction: ((response: WretchOptions) => boolean)
2 | -------------------------------------------------------------------------------- /docs/api/types/middlewares_throttlingCache.ThrottlingCacheKeyFunction.html: -------------------------------------------------------------------------------- 1 | ThrottlingCacheKeyFunction | wretch
ThrottlingCacheKeyFunction: ((url: string, opts: WretchOptions) => string)
2 | -------------------------------------------------------------------------------- /docs/api/types/middlewares_throttlingCache.ThrottlingCacheSkipFunction.html: -------------------------------------------------------------------------------- 1 | ThrottlingCacheSkipFunction | wretch
ThrottlingCacheSkipFunction: ((url: string, opts: WretchOptions) => boolean)
2 | -------------------------------------------------------------------------------- /docs/assets/ts-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | TypeScript logo 4 | 5 | 6 | -------------------------------------------------------------------------------- /docs/assets/wretch.svg: -------------------------------------------------------------------------------- 1 | Layer 1 -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import eslint from "@eslint/js" 4 | import tseslint from "typescript-eslint" 5 | import globals from "globals" 6 | 7 | delete globals.browser["AudioWorkletGlobalScope "] 8 | 9 | export default tseslint.config( 10 | eslint.configs.recommended, 11 | ...tseslint.configs.recommended, 12 | { 13 | rules: { 14 | indent: ["error", 2], 15 | "linebreak-style": ["error", "unix"], 16 | quotes: ["error", "double"], 17 | semi: ["error", "never"], 18 | "no-console": "warn", 19 | "arrow-parens": ["error", "as-needed"], 20 | "no-var": "error", 21 | "prefer-const": "error", 22 | "object-curly-spacing": ["error", "always"], 23 | "@typescript-eslint/no-explicit-any": "off", 24 | "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }], 25 | }, 26 | languageOptions: { 27 | globals: { 28 | ...globals.browser, 29 | "AudioWorkletGlobalScope": false 30 | } 31 | }, 32 | ignores: [ "dist/"], 33 | } 34 | ) 35 | -------------------------------------------------------------------------------- /karma.conf.cjs: -------------------------------------------------------------------------------- 1 | module.exports = function (config) { 2 | config.set({ 3 | files: [ 4 | 'dist/bundle/wretch.all.min.js', 5 | 'test/browser/*.spec.js' 6 | ], 7 | frameworks: ['jasmine'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-firefox-launcher'), 12 | // require('karma-safari-launcher'), 13 | require('karma-safarinative-launcher'), 14 | ], 15 | reporters: ['progress'], 16 | port: 9877, 17 | colors: true, 18 | logLevel: config.LOG_INFO, 19 | browsers: ['ChromeHeadless', 'FirefoxHeadless', 'SafariNative'], 20 | autoWatch: false, 21 | concurrency: 1 22 | }) 23 | } 24 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import typescript from '@rollup/plugin-typescript'; 2 | import terser from '@rollup/plugin-terser'; 3 | import { nodeResolve } from '@rollup/plugin-node-resolve'; 4 | 5 | const addons = ["abort", "basicAuth", "formData", "formUrl", "perfs", "queryString", "progress"] 6 | const middlewares = ["dedupe", "delay", "retry", "throttlingCache"] 7 | 8 | const common = { 9 | plugins: [ 10 | typescript({ 11 | importHelpers: true 12 | }), 13 | nodeResolve(), 14 | terser({ 15 | ecma: 2018, 16 | output: { 17 | comments: false, 18 | }, 19 | compress: { 20 | booleans_as_integers: true, 21 | passes: 2 22 | } 23 | }) 24 | ], 25 | external: ["url"] 26 | } 27 | 28 | const formats = ["umd", "cjs", "esm"] 29 | const outputs = output => formats.map(format => ({ 30 | ...output, 31 | format, 32 | file: 33 | format === "cjs" ? output.file.replace(".js", ".cjs") : 34 | format === "esm" ? output.file.replace(".js", ".mjs") : 35 | output.file 36 | })) 37 | 38 | export default [ 39 | { 40 | input: "./src/index.ts", 41 | output: outputs({ 42 | file: "dist/bundle/wretch.min.js", 43 | format: "umd", 44 | name: "wretch", 45 | exports: "default", 46 | sourcemap: true 47 | }), 48 | ...common 49 | }, 50 | { 51 | input: "./src/index.all.ts", 52 | output: [ 53 | ...outputs({ 54 | file: "dist/bundle/wretch.all.min.js", 55 | format: "umd", 56 | name: "wretch", 57 | exports: "default", 58 | sourcemap: true 59 | }) 60 | ], 61 | ...common 62 | }, 63 | ...addons.map(addon => ({ 64 | input: `./src/addons/${addon}.ts`, 65 | output: outputs({ 66 | file: `dist/bundle/addons/${addon}.min.js`, 67 | format: "umd", 68 | name: `wretch${addon.charAt(0).toLocaleUpperCase() + addon.slice(1)}Addon`, 69 | exports: "default", 70 | sourcemap: true 71 | }), 72 | ...common 73 | })), 74 | ...middlewares.map(middleware => ({ 75 | input: `./src/middlewares/${middleware}.ts`, 76 | output: outputs({ 77 | file: `dist/bundle/middlewares/${middleware}.min.js`, 78 | format: "umd", 79 | name: `wretch${middleware.charAt(0).toLocaleUpperCase() + middleware.slice(1)}Middleware`, 80 | exports: "named", 81 | sourcemap: true 82 | }), 83 | ...common 84 | })) 85 | ] 86 | -------------------------------------------------------------------------------- /scripts/conventional-changelog-wretch/index.js: -------------------------------------------------------------------------------- 1 | const resolve = require("path").resolve 2 | const Q = require("q") 3 | const readFile = Q.denodeify(require("fs").readFile) 4 | 5 | const emojiMatch = { 6 | ":fire:": "Breaking change(s)", 7 | ":bug:": "Bug fix(es)", 8 | ":factory:": "New feature(s)", 9 | ":art:": "Code improvement(s)", 10 | ":checkered_flag:": "Performance update(s)", 11 | ":white_check_mark:": "Test improvement(s)", 12 | ":memo:": "Documentation update(s)", 13 | ":arrow_up:": "Version update(s)" 14 | } 15 | 16 | function presetOpts(cb) { 17 | 18 | const parserOpts = { 19 | headerPattern: /^(:.*?:) (.*)$/, 20 | headerCorrespondence: [ 21 | "emoji", 22 | "shortDesc" 23 | ] 24 | } 25 | 26 | const writerOpts = { 27 | transform: function (commit) { 28 | if (!commit.emoji || typeof commit.emoji !== "string") 29 | return 30 | 31 | const emojiText = emojiMatch[commit.emoji]; 32 | commit.emoji = commit.emoji.substring(0, 72); 33 | const emojiLength = commit.emoji.length; 34 | 35 | if (typeof commit.hash === 'string') { 36 | commit.hash = commit.hash.substring(0, 7); 37 | } 38 | 39 | if (typeof commit.shortDesc === 'string') { 40 | commit.shortDesc = commit.shortDesc.substring(0, 72 - emojiLength); 41 | } 42 | 43 | commit.emoji = commit.emoji + " " + emojiText 44 | 45 | return commit 46 | }, 47 | groupBy: "emoji", 48 | commitGroupsSort: "title", 49 | commitsSort: ["emoji", "shortDesc"] 50 | } 51 | 52 | Q.all([ 53 | readFile(resolve(__dirname, "templates/template.hbs"), "utf-8"), 54 | readFile(resolve(__dirname, "templates/header.hbs"), "utf-8"), 55 | readFile(resolve(__dirname, "templates/commit.hbs"), "utf-8") 56 | ]).spread(function (template, header, commit) { 57 | writerOpts.mainTemplate = template 58 | writerOpts.headerPartial = header 59 | writerOpts.commitPartial = commit 60 | 61 | cb(null, { 62 | parserOpts: parserOpts, 63 | writerOpts: writerOpts 64 | }) 65 | }) 66 | } 67 | 68 | module.exports = presetOpts 69 | -------------------------------------------------------------------------------- /scripts/conventional-changelog-wretch/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "conventional-changelog-wretch", 3 | "private": true, 4 | "version": "0.0.1", 5 | "description": "conventional-changelog wretch preset", 6 | "main": "index.js", 7 | "files": [ 8 | "index.js", 9 | "templates" 10 | ] 11 | } -------------------------------------------------------------------------------- /scripts/conventional-changelog-wretch/templates/commit.hbs: -------------------------------------------------------------------------------- 1 | * {{#if shortDesc}}{{shortDesc}}{{else}}{{header}}{{/if}} 2 | 3 | {{~!-- commit hash --}} {{#if @root.linkReferences}}([{{hash}}]({{#if @root.host}}{{@root.host}}/{{/if}}{{#if @root.owner}}{{@root.owner}}/{{/if}}{{@root.repository}}/{{@root.commit}}/{{hash}})){{else}}{{hash~}}{{/if}} 4 | 5 | {{~!-- commit references --}}{{#if references}}, closes{{~#each references}} {{#if @root.linkReferences}}[{{#if this.owner}}{{this.owner}}/{{/if}}{{this.repository}}#{{this.issue}}]({{#if @root.host}}{{@root.host}}/{{/if}}{{#if this.repository}}{{#if this.owner}}{{this.owner}}/{{/if}}{{this.repository}}{{else}}{{#if @root.owner}}{{@root.owner}}/{{/if}}{{@root.repository}}{{/if}}/{{@root.issue}}/{{this.issue}}){{else}}{{#if this.owner}}{{this.owner}}/{{/if}}{{this.repository}}#{{this.issue}}{{/if}}{{/each}}{{/if}} 6 | -------------------------------------------------------------------------------- /scripts/conventional-changelog-wretch/templates/header.hbs: -------------------------------------------------------------------------------- 1 | 2 | {{#if isPatch}}##{{else}}#{{/if}} {{#if @root.linkCompare}}[{{version}}]({{@root.host}}/{{#if @root.owner}}{{@root.owner}}/{{/if}}{{@root.repository}}/compare/{{previousTag}}...{{currentTag}}){{else}}{{version}}{{/if}}{{#if title}} "{{title}}"{{/if}}{{#if date}} ({{date}}){{/if}} 3 | -------------------------------------------------------------------------------- /scripts/conventional-changelog-wretch/templates/template.hbs: -------------------------------------------------------------------------------- 1 | {{> header}} 2 | 3 | {{#each commitGroups}} 4 | 5 | {{#if title}} 6 | ### {{title}} 7 | 8 | {{/if}} 9 | {{#each commits}} 10 | {{> commit root=@root}} 11 | {{/each}} 12 | {{/each}} 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /scripts/mockServer.js: -------------------------------------------------------------------------------- 1 | import { launch } from "../test/mock.js" 2 | 3 | launch(9876) 4 | -------------------------------------------------------------------------------- /src/addons/abort.ts: -------------------------------------------------------------------------------- 1 | import type { Wretch, WretchAddon, WretchErrorCallback, WretchResponseChain } from "../types.js" 2 | 3 | export interface AbortWretch { 4 | /** 5 | * Associates a custom controller with the request. 6 | * 7 | * Useful when you need to use 8 | * your own AbortController, otherwise wretch will create a new controller itself. 9 | * 10 | * ```js 11 | * const controller = new AbortController() 12 | * 13 | * // Associates the same controller with multiple requests 14 | * wretch("url1") 15 | * .addon(AbortAddon()) 16 | * .signal(controller) 17 | * .get() 18 | * .json() 19 | * wretch("url2") 20 | * .addon(AbortAddon()) 21 | * .signal(controller) 22 | * .get() 23 | * .json() 24 | * 25 | * // Aborts both requests 26 | * controller.abort() 27 | * ``` 28 | * 29 | * @param controller - An instance of AbortController 30 | */ 31 | signal: (this: T & Wretch, controller: AbortController) => this 32 | } 33 | 34 | export interface AbortResolver { 35 | /** 36 | * Aborts the request after a fixed time. 37 | * 38 | * If you use a custom AbortController associated with the request, pass it as the second argument. 39 | * 40 | * ```js 41 | * // 1 second timeout 42 | * wretch("...").addon(AbortAddon()).get().setTimeout(1000).json(_ => 43 | * // will not be called if the request timeouts 44 | * ) 45 | * ``` 46 | * 47 | * @param time - Time in milliseconds 48 | * @param controller - An instance of AbortController 49 | */ 50 | setTimeout: (this: C & WretchResponseChain, time: number, controller?: AbortController) => this 51 | /** 52 | * Returns the provided or generated AbortController plus the wretch response chain as a pair. 53 | * 54 | * ```js 55 | * // We need the controller outside the chain 56 | * const [c, w] = wretch("url") 57 | * .addon(AbortAddon()) 58 | * .get() 59 | * .controller() 60 | * 61 | * // Resume with the chain 62 | * w.onAbort(_ => console.log("ouch")).json() 63 | * 64 | * // Later on… 65 | * c.abort() 66 | * ``` 67 | */ 68 | controller: (this: C & WretchResponseChain) => [any, this] 69 | /** 70 | * Catches an AbortError and performs a callback. 71 | */ 72 | onAbort: (this: C & WretchResponseChain, cb: WretchErrorCallback) => this 73 | } 74 | 75 | /** 76 | * Adds the ability to abort requests using AbortController and signals under the hood. 77 | * 78 | * 79 | * _Only compatible with browsers that support 80 | * [AbortControllers](https://developer.mozilla.org/en-US/docs/Web/API/AbortController). 81 | * Otherwise, you could use a (partial) 82 | * [polyfill](https://www.npmjs.com/package/abortcontroller-polyfill)._ 83 | * 84 | * ```js 85 | * import AbortAddon from "wretch/addons/abort" 86 | * 87 | * const [c, w] = wretch("...") 88 | * .addon(AbortAddon()) 89 | * .get() 90 | * .onAbort((_) => console.log("Aborted !")) 91 | * .controller(); 92 | * 93 | * w.text((_) => console.log("should never be called")); 94 | * c.abort(); 95 | * 96 | * // Or : 97 | * 98 | * const controller = new AbortController(); 99 | * 100 | * wretch("...") 101 | * .addon(AbortAddon()) 102 | * .signal(controller) 103 | * .get() 104 | * .onAbort((_) => console.log("Aborted !")) 105 | * .text((_) => console.log("should never be called")); 106 | * 107 | * controller.abort(); 108 | * ``` 109 | */ 110 | const abort: () => WretchAddon = () => { 111 | return { 112 | beforeRequest(wretch, options, state) { 113 | const fetchController = wretch._config.polyfill("AbortController", false, true) 114 | if (!options["signal"] && fetchController) { 115 | options["signal"] = fetchController.signal 116 | } 117 | const timeout = { 118 | ref: null, 119 | clear() { 120 | if (timeout.ref) { 121 | clearTimeout(timeout.ref) 122 | timeout.ref = null 123 | } 124 | } 125 | } 126 | state.abort = { 127 | timeout, 128 | fetchController 129 | } 130 | return wretch 131 | }, 132 | wretch: { 133 | signal(controller) { 134 | return { ...this, _options: { ...this._options, signal: controller.signal } } 135 | }, 136 | }, 137 | resolver: { 138 | setTimeout(time, controller = this._sharedState.abort.fetchController) { 139 | const { timeout } = this._sharedState.abort 140 | timeout.clear() 141 | timeout.ref = setTimeout(() => controller.abort(), time) 142 | return this 143 | }, 144 | controller() { return [this._sharedState.abort.fetchController, this] }, 145 | onAbort(cb) { return this.error("AbortError", cb) } 146 | }, 147 | } 148 | } 149 | 150 | export default abort 151 | -------------------------------------------------------------------------------- /src/addons/basicAuth.ts: -------------------------------------------------------------------------------- 1 | import type { Config, ConfiguredMiddleware, Wretch, WretchAddon } from "../types.js" 2 | 3 | function utf8ToBase64(input: string) { 4 | const utf8Bytes = new TextEncoder().encode(input) 5 | return btoa(String.fromCharCode(...utf8Bytes)) 6 | } 7 | 8 | export interface BasicAuthAddon { 9 | /** 10 | * Sets the `Authorization` header to `Basic ` + . 11 | * Additionally, allows using URLs with credentials in them. 12 | * 13 | * ```js 14 | * const user = "user" 15 | * const pass = "pass" 16 | * 17 | * // Automatically sets the Authorization header to "Basic " + 18 | * wretch("...").addon(BasicAuthAddon).basicAuth(user, pass).get() 19 | * 20 | * // Allows using URLs with credentials in them 21 | * wretch(`https://${user}:${pass}@...`).addon(BasicAuthAddon).get() 22 | * ``` 23 | * 24 | * @param username - Username to use for basic auth 25 | * @param password - Password to use for basic auth 26 | */ 27 | basicAuth( 28 | this: T & Wretch, 29 | username: string, 30 | password: string 31 | ): this 32 | } 33 | 34 | const makeBasicAuthMiddleware: (config: Config) => ConfiguredMiddleware = config => next => (url, opts) => { 35 | const _URL = config.polyfill("URL") 36 | let parsedUrl: URL | null 37 | try { 38 | parsedUrl = new _URL(url) 39 | } catch { 40 | parsedUrl = null 41 | } 42 | 43 | if (parsedUrl?.username || parsedUrl?.password) { 44 | const basicAuthBase64 = utf8ToBase64( 45 | `${decodeURIComponent(parsedUrl.username)}:${decodeURIComponent(parsedUrl.password)}`, 46 | ) 47 | opts.headers = { 48 | ...opts.headers, 49 | Authorization: `Basic ${basicAuthBase64}`, 50 | } 51 | parsedUrl.username = "" 52 | parsedUrl.password = "" 53 | url = parsedUrl.toString() 54 | } 55 | 56 | return next(url, opts) 57 | } 58 | 59 | 60 | /** 61 | * Adds the ability to use basic auth with the `Authorization` header. 62 | * 63 | * ```js 64 | * import BasicAuthAddon from "wretch/addons/basicAuth" 65 | * 66 | * wretch().addon(BasicAuthAddon) 67 | * ``` 68 | */ 69 | const basicAuth: WretchAddon = { 70 | beforeRequest(wretch) { 71 | return wretch.middlewares([makeBasicAuthMiddleware(wretch._config)]) 72 | }, 73 | wretch: { 74 | basicAuth(username, password) { 75 | const basicAuthBase64 = utf8ToBase64(`${username}:${password}`) 76 | return this.auth(`Basic ${basicAuthBase64}`) 77 | }, 78 | }, 79 | } 80 | 81 | export default basicAuth 82 | -------------------------------------------------------------------------------- /src/addons/formData.ts: -------------------------------------------------------------------------------- 1 | import type { Wretch, Config, WretchAddon } from "../types.js" 2 | 3 | function convertFormData( 4 | formObject: object, 5 | recursive: string[] | boolean = false, 6 | config: Config, 7 | formData = config.polyfill("FormData", true, true), 8 | ancestors = [] as string[], 9 | ) { 10 | Object.entries(formObject).forEach(([key, value]) => { 11 | let formKey = ancestors.reduce((acc, ancestor) => ( 12 | acc ? `${acc}[${ancestor}]` : ancestor 13 | ), null) 14 | formKey = formKey ? `${formKey}[${key}]` : key 15 | if (value instanceof Array || (globalThis.FileList && value instanceof FileList)) { 16 | for (const item of value as File[]) 17 | formData.append(formKey, item) 18 | } else if ( 19 | recursive && 20 | typeof value === "object" && 21 | ( 22 | !(recursive instanceof Array) || 23 | !recursive.includes(key) 24 | ) 25 | ) { 26 | if (value !== null) { 27 | convertFormData(value, recursive, config, formData, [...ancestors, key]) 28 | } 29 | } else { 30 | formData.append(formKey, value) 31 | } 32 | }) 33 | 34 | return formData 35 | } 36 | 37 | export interface FormDataAddon { 38 | /** 39 | * Converts the javascript object to a FormData and sets the request body. 40 | * 41 | * ```js 42 | * const form = { 43 | * hello: "world", 44 | * duck: "Muscovy", 45 | * }; 46 | * 47 | * wretch("...").addons(FormDataAddon).formData(form).post(); 48 | * ``` 49 | * 50 | * The `recursive` argument when set to `true` will enable recursion through all 51 | * nested objects and produce `object[key]` keys. It can be set to an array of 52 | * string to exclude specific keys. 53 | * 54 | * > Warning: Be careful to exclude `Blob` instances in the Browser, and 55 | * > `ReadableStream` and `Buffer` instances when using the node.js compatible 56 | * > `form-data` package. 57 | * 58 | * ```js 59 | * const form = { 60 | * duck: "Muscovy", 61 | * duckProperties: { 62 | * beak: { 63 | * color: "yellow", 64 | * }, 65 | * legs: 2, 66 | * }, 67 | * ignored: { 68 | * key: 0, 69 | * }, 70 | * }; 71 | * 72 | * // Will append the following keys to the FormData payload: 73 | * // "duck", "duckProperties[beak][color]", "duckProperties[legs]" 74 | * wretch("...").addons(FormDataAddon).formData(form, ["ignored"]).post(); 75 | * ``` 76 | * 77 | * > Note: This addon does not support specifying a custom `filename`. 78 | * > If you need to do so, you can use the `body` method directly: 79 | * > ```js 80 | * > const form = new FormData(); 81 | * > form.append("hello", "world", "hello.txt"); 82 | * > wretch("...").body(form).post(); 83 | * > ``` 84 | * > See: https://developer.mozilla.org/en-US/docs/Web/API/FormData/append#example 85 | * 86 | * @param formObject - An object which will be converted to a FormData 87 | * @param recursive - If `true`, will recurse through all nested objects. Can be set as an array of string to exclude specific keys. 88 | */ 89 | formData(this: T & Wretch, formObject: object, recursive?: string[] | boolean): this 90 | } 91 | 92 | /** 93 | * Adds the ability to convert a an object to a FormData and use it as a request body. 94 | * 95 | * ```js 96 | * import FormDataAddon from "wretch/addons/formData" 97 | * 98 | * wretch().addon(FormDataAddon) 99 | * ``` 100 | */ 101 | const formData: WretchAddon = { 102 | wretch: { 103 | formData(formObject, recursive = false) { 104 | return this.body(convertFormData(formObject, recursive, this._config)) 105 | } 106 | } 107 | } 108 | 109 | export default formData 110 | -------------------------------------------------------------------------------- /src/addons/formUrl.ts: -------------------------------------------------------------------------------- 1 | import type { Wretch, WretchAddon } from "../types.js" 2 | 3 | function encodeQueryValue(key: string, value: unknown) { 4 | return encodeURIComponent(key) + 5 | "=" + 6 | encodeURIComponent( 7 | typeof value === "object" ? 8 | JSON.stringify(value) : 9 | "" + value 10 | ) 11 | } 12 | function convertFormUrl(formObject: object) { 13 | return Object.keys(formObject) 14 | .map(key => { 15 | const value = formObject[key] 16 | if (value instanceof Array) { 17 | return value.map(v => encodeQueryValue(key, v)).join("&") 18 | } 19 | return encodeQueryValue(key, value) 20 | }) 21 | .join("&") 22 | } 23 | 24 | export interface FormUrlAddon { 25 | /** 26 | * Converts the input parameter to an url encoded string and sets the content-type 27 | * header and body. If the input argument is already a string, skips the conversion 28 | * part. 29 | * 30 | * ```js 31 | * const form = { a: 1, b: { c: 2 } }; 32 | * const alreadyEncodedForm = "a=1&b=%7B%22c%22%3A2%7D"; 33 | * 34 | * // Automatically sets the content-type header to "application/x-www-form-urlencoded" 35 | * wretch("...").addon(FormUrlAddon).formUrl(form).post(); 36 | * wretch("...").addon(FormUrlAddon).formUrl(alreadyEncodedForm).post(); 37 | * ``` 38 | * 39 | * @param input - An object to convert into an url encoded string or an already encoded string 40 | */ 41 | formUrl(this: T & Wretch, input: (object | string)): this 42 | } 43 | 44 | /** 45 | * Adds the ability to convert a an object to a FormUrl and use it as a request body. 46 | * 47 | * ```js 48 | * import FormUrlAddon from "wretch/addons/formUrl" 49 | * 50 | * wretch().addon(FormUrlAddon) 51 | * ``` 52 | */ 53 | const formUrl: WretchAddon = { 54 | wretch: { 55 | formUrl(input) { 56 | return this 57 | .body(typeof input === "string" ? input : convertFormUrl(input)) 58 | .content("application/x-www-form-urlencoded") 59 | } 60 | } 61 | } 62 | 63 | export default formUrl 64 | -------------------------------------------------------------------------------- /src/addons/index.ts: -------------------------------------------------------------------------------- 1 | export { default as abortAddon } from "./abort.js" 2 | export type { AbortWretch, AbortResolver } from "./abort.js" 3 | export { default as basicAuthAddon } from "./basicAuth.js" 4 | export type { BasicAuthAddon } from "./basicAuth.js" 5 | export { default as formDataAddon } from "./formData.js" 6 | export type { FormDataAddon } from "./formData.js" 7 | export { default as formUrlAddon } from "./formUrl.js" 8 | export type { FormUrlAddon } from "./formUrl.js" 9 | export { default as perfsAddon } from "./perfs.js" 10 | export type { PerfsAddon } from "./perfs.js" 11 | export { default as queryStringAddon } from "./queryString.js" 12 | export type { QueryStringAddon } from "./queryString.js" 13 | export { default as progressAddon } from "./progress.js" 14 | export type { ProgressResolver } from "./progress.js" 15 | -------------------------------------------------------------------------------- /src/addons/perfs.ts: -------------------------------------------------------------------------------- 1 | import type { WretchResponseChain, WretchAddon, Config } from "../types.js" 2 | 3 | export type PerfCallback = (timing: any) => void 4 | 5 | export interface PerfsAddon { 6 | /** 7 | * Performs a callback on the API performance timings of the request. 8 | * 9 | * Warning: Still experimental on browsers and node.js 10 | */ 11 | perfs: (this: C & WretchResponseChain, cb?: PerfCallback) => this, 12 | } 13 | 14 | /** 15 | * Adds the ability to measure requests using the Performance Timings API. 16 | * 17 | * Uses the Performance API 18 | * ([browsers](https://developer.mozilla.org/en-US/docs/Web/API/Performance_API) & 19 | * [node.js](https://nodejs.org/api/perf_hooks.html)) to expose timings related to 20 | * the underlying request. 21 | * 22 | * Browser timings are very accurate, node.js only contains raw measures. 23 | * 24 | * ```js 25 | * import PerfsAddon from "wretch/addons/perfs" 26 | * 27 | * // Use perfs() before the response types (text, json, ...) 28 | * wretch("...") 29 | * .addon(PerfsAddon()) 30 | * .get() 31 | * .perfs((timings) => { 32 | * // Will be called when the timings are ready. 33 | * console.log(timings.startTime); 34 | * }) 35 | * .res(); 36 | * 37 | * ``` 38 | * 39 | * For node.js, there is a little extra work to do : 40 | * 41 | * ```js 42 | * // Node.js only 43 | * const { performance, PerformanceObserver } = require("perf_hooks"); 44 | * 45 | * wretch.polyfills({ 46 | * fetch: function (url, opts) { 47 | * performance.mark(url + " - begin"); 48 | * return fetch(url, opts).then(res => { 49 | * performance.mark(url + " - end"); 50 | * setTimeout(() => performance.measure(res.url, url + " - begin", url + " - end"), 0); 51 | * return res; 52 | * }); 53 | * }, 54 | * // other polyfills… 55 | * performance: performance, 56 | * PerformanceObserver: PerformanceObserver, 57 | * }); 58 | * ``` 59 | */ 60 | const perfs: () => WretchAddon = () => { 61 | const callbacks = new Map() 62 | let observer /*: PerformanceObserver | null*/ = null 63 | 64 | const onMatch = ( 65 | entries /*: PerformanceObserverEntryList */, 66 | name: string, 67 | callback: PerfCallback, 68 | performance: typeof globalThis.performance 69 | ) => { 70 | if (!entries.getEntriesByName) 71 | return false 72 | const matches = entries.getEntriesByName(name) 73 | if (matches && matches.length > 0) { 74 | callback(matches.reverse()[0]) 75 | if (performance.clearMeasures) 76 | performance.clearMeasures(name) 77 | callbacks.delete(name) 78 | 79 | if (callbacks.size < 1) { 80 | observer.disconnect() 81 | if (performance.clearResourceTimings) { 82 | performance.clearResourceTimings() 83 | } 84 | } 85 | return true 86 | } 87 | return false 88 | } 89 | 90 | const initObserver = ( 91 | performance: (typeof globalThis.performance) | null | undefined, 92 | performanceObserver /*: (typeof PerformanceObserver) | null | undefined */ 93 | ) => { 94 | if (!observer && performance && performanceObserver) { 95 | observer = new performanceObserver(entries => { 96 | callbacks.forEach((callback, name) => { 97 | onMatch(entries, name, callback, performance) 98 | }) 99 | }) 100 | if (performance.clearResourceTimings) { 101 | performance.clearResourceTimings() 102 | } 103 | } 104 | 105 | return observer 106 | } 107 | 108 | const monitor = ( 109 | name: string | null | undefined, 110 | callback: PerfCallback | null | undefined, 111 | config: Config 112 | ) => { 113 | if (!name || !callback) 114 | return 115 | 116 | const performance = config.polyfill("performance", false) 117 | const performanceObserver = config.polyfill("PerformanceObserver", false) 118 | 119 | if (!initObserver(performance, performanceObserver)) 120 | return 121 | 122 | if (!onMatch(performance, name, callback, performance)) { 123 | if (callbacks.size < 1) 124 | observer.observe({ entryTypes: ["resource", "measure"] }) 125 | callbacks.set(name, callback) 126 | } 127 | } 128 | 129 | return { 130 | resolver: { 131 | perfs(cb) { 132 | this._fetchReq 133 | .then(() => 134 | monitor(this._wretchReq._url, cb, this._wretchReq._config) 135 | ) 136 | .catch(() => {/* swallow */ }) 137 | return this 138 | }, 139 | } 140 | } 141 | } 142 | 143 | export default perfs 144 | -------------------------------------------------------------------------------- /src/addons/progress.ts: -------------------------------------------------------------------------------- 1 | import type { ConfiguredMiddleware, WretchAddon, WretchResponseChain } from "../types.js" 2 | 3 | export interface ProgressResolver { 4 | /** 5 | * Provides a way to register a callback to be invoked one or multiple times during the download. 6 | * The callback receives the current progress as two arguments, the number of bytes loaded and the total number of bytes to load. 7 | * 8 | * _Under the hood: this method adds a middleware to the chain that will intercept the response and replace the body with a new one that will emit the progress event._ 9 | * 10 | * ```js 11 | * import ProgressAddon from "wretch/addons/progress" 12 | * 13 | * wretch("some_url") 14 | * // Register the addon 15 | * .addon(ProgressAddon()) 16 | * .get() 17 | * // Log the progress as a percentage of completion 18 | * .progress((loaded, total) => console.log(`${(loaded / total * 100).toFixed(0)}%`)) 19 | * ``` 20 | * 21 | * @param onProgress - A callback that will be called one or multiple times with the number of bytes loaded and the total number of bytes to load. 22 | */ 23 | progress: ( 24 | this: C & WretchResponseChain, 25 | onProgress: (loaded: number, total: number) => void 26 | ) => this 27 | } 28 | 29 | /** 30 | * Adds the ability to monitor progress when downloading a response. 31 | * 32 | * _Compatible with all platforms implementing the [TransformStream WebAPI](https://developer.mozilla.org/en-US/docs/Web/API/TransformStream#browser_compatibility)._ 33 | * 34 | * ```js 35 | * import ProgressAddon from "wretch/addons/progress" 36 | * 37 | * wretch("some_url") 38 | * // Register the addon 39 | * .addon(ProgressAddon()) 40 | * .get() 41 | * // Log the progress as a percentage of completion 42 | * .progress((loaded, total) => console.log(`${(loaded / total * 100).toFixed(0)}%`)) 43 | * ``` 44 | */ 45 | const progress: () => WretchAddon = () => { 46 | function transformMiddleware(state: Record) : ConfiguredMiddleware { 47 | return next => (url, opts) => { 48 | let loaded = 0 49 | let total = 0 50 | return next(url, opts).then(response => { 51 | try { 52 | const contentLength = response.headers.get("content-length") 53 | total = contentLength ? +contentLength : null 54 | const transform = new TransformStream({ 55 | transform(chunk, controller) { 56 | loaded += chunk.length 57 | if (total < loaded) { 58 | total = loaded 59 | } 60 | if (state.progress) { 61 | state.progress(loaded, total) 62 | } 63 | controller.enqueue(chunk) 64 | } 65 | }) 66 | return new Response(response.body.pipeThrough(transform), response) 67 | } catch (e) { 68 | return response 69 | } 70 | }) 71 | } 72 | } 73 | 74 | return { 75 | beforeRequest(wretch, _, state) { 76 | return wretch.middlewares([transformMiddleware(state)]) 77 | }, 78 | resolver: { 79 | progress(onProgress: (loaded: number, total: number) => void) { 80 | this._sharedState.progress = onProgress 81 | return this 82 | } 83 | }, 84 | } 85 | } 86 | 87 | export default progress 88 | -------------------------------------------------------------------------------- /src/addons/queryString.ts: -------------------------------------------------------------------------------- 1 | import type { Wretch, Config, WretchAddon } from "../types.js" 2 | 3 | function stringify(value?: string | null): string | null { 4 | return typeof value !== "undefined" ? value : "" 5 | } 6 | 7 | const appendQueryParams = (url: string, qp: object | string, replace: boolean, omitUndefinedOrNullValues: boolean, config: Config) => { 8 | let queryString: string 9 | 10 | if (typeof qp === "string") { 11 | queryString = qp 12 | } else { 13 | const usp = config.polyfill("URLSearchParams", true, true) 14 | for (const key in qp) { 15 | const value = qp[key] 16 | if (omitUndefinedOrNullValues && (value === null || value === undefined)) continue 17 | if (qp[key] instanceof Array) { 18 | for (const val of value) 19 | usp.append(key, stringify(val)) 20 | } else { 21 | usp.append(key, stringify(value)) 22 | } 23 | } 24 | queryString = usp.toString() 25 | } 26 | 27 | const split = url.split("?") 28 | 29 | if (!queryString) 30 | return replace ? split[0] : url 31 | 32 | if (replace || split.length < 2) 33 | return split[0] + "?" + queryString 34 | 35 | return url + "&" + queryString 36 | } 37 | 38 | export interface QueryStringAddon { 39 | /** 40 | * Converts a javascript object to query parameters, then appends this query string 41 | * to the current url. String values are used as the query string verbatim. 42 | * 43 | * Pass `true` as the second argument to replace existing query parameters. 44 | * Pass `true` as the third argument to completely omit the key=value pair for undefined or null values. 45 | * 46 | * ``` 47 | * import QueryAddon from "wretch/addons/queryString" 48 | * 49 | * let w = wretch("http://example.com").addon(QueryStringAddon); 50 | * // url is http://example.com 51 | * w = w.query({ a: 1, b: 2 }); 52 | * // url is now http://example.com?a=1&b=2 53 | * w = w.query({ c: 3, d: [4, 5] }); 54 | * // url is now http://example.com?a=1&b=2c=3&d=4&d=5 55 | * w = w.query("five&six&seven=eight"); 56 | * // url is now http://example.com?a=1&b=2c=3&d=4&d=5&five&six&seven=eight 57 | * w = w.query({ reset: true }, true); 58 | * // url is now http://example.com?reset=true 59 | * ``` 60 | * 61 | * ##### **Note that .query is not meant to handle complex cases with nested objects.** 62 | * 63 | * For this kind of usage, you can use `wretch` in conjunction with other libraries 64 | * (like [`qs`](https://github.com/ljharb/qs)). 65 | * 66 | * ```js 67 | * // Using wretch with qs 68 | * 69 | * const queryObject = { some: { nested: "objects" } }; 70 | * const w = wretch("https://example.com/").addon(QueryStringAddon) 71 | * 72 | * // Use .qs inside .query : 73 | * 74 | * w.query(qs.stringify(queryObject)); 75 | * 76 | * // Use .defer : 77 | * 78 | * const qsWretch = w.defer((w, url, { qsQuery, qsOptions }) => ( 79 | * qsQuery ? w.query(qs.stringify(qsQuery, qsOptions)) : w 80 | * )); 81 | * 82 | * qsWretch 83 | * .url("https://example.com/") 84 | * .options({ qs: { query: queryObject } }); 85 | * ``` 86 | * 87 | * @param qp - An object which will be converted, or a string which will be used verbatim. 88 | */ 89 | query(this: T & Wretch, qp: object | string, replace?: boolean, omitUndefinedOrNullValues?: boolean): this 90 | } 91 | 92 | /** 93 | * Adds the ability to append query parameters from a javascript object. 94 | * 95 | * ```js 96 | * import QueryAddon from "wretch/addons/queryString" 97 | * 98 | * wretch().addon(QueryAddon) 99 | * ``` 100 | */ 101 | const queryString: WretchAddon = { 102 | wretch: { 103 | query(qp, replace = false, omitUndefinedOrNullValues = false) { 104 | return { ...this, _url: appendQueryParams(this._url, qp, replace, omitUndefinedOrNullValues, this._config) } 105 | } 106 | } 107 | } 108 | 109 | export default queryString 110 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import { mix } from "./utils.js" 2 | import type { Config, ErrorType, WretchOptions } from "./types.js" 3 | 4 | declare const global 5 | 6 | const config: Config = { 7 | // Default options 8 | options: {}, 9 | // Error type 10 | errorType: "text", 11 | // Polyfills 12 | polyfills: { 13 | // fetch: null, 14 | // FormData: null, 15 | // URL: null, 16 | // URLSearchParams: null, 17 | // performance: null, 18 | // PerformanceObserver: null, 19 | // AbortController: null, 20 | }, 21 | polyfill(p: string, doThrow: boolean = true, instance: boolean = false, ...args: any[]) { 22 | const res = this.polyfills[p] || 23 | (typeof self !== "undefined" ? self[p] : null) || 24 | (typeof global !== "undefined" ? global[p] : null) 25 | if (doThrow && !res) throw new Error(p + " is not defined") 26 | return instance && res ? new res(...args) : res 27 | } 28 | } 29 | 30 | /** 31 | * Sets the default fetch options that will be stored internally when instantiating wretch objects. 32 | * 33 | * ```js 34 | * import wretch from "wretch" 35 | * 36 | * wretch.options({ headers: { "Accept": "application/json" } }); 37 | * 38 | * // The fetch request is sent with both headers. 39 | * wretch("...", { headers: { "X-Custom": "Header" } }).get().res(); 40 | * ``` 41 | * 42 | * @param options Default options 43 | * @param replace If true, completely replaces the existing options instead of mixing in 44 | */ 45 | export function setOptions(options: WretchOptions, replace = false) { 46 | config.options = replace ? options : mix(config.options, options) 47 | } 48 | 49 | /** 50 | * Sets the default polyfills that will be stored internally when instantiating wretch objects. 51 | * Useful for browserless environments like `node.js`. 52 | * 53 | * Needed for libraries like [fetch-ponyfill](https://github.com/qubyte/fetch-ponyfill). 54 | * 55 | * ```js 56 | * import wretch from "wretch" 57 | * 58 | * wretch.polyfills({ 59 | * fetch: require("node-fetch"), 60 | * FormData: require("form-data"), 61 | * URLSearchParams: require("url").URLSearchParams, 62 | * }); 63 | * 64 | * // Uses the above polyfills. 65 | * wretch("...").get().res(); 66 | * ``` 67 | * 68 | * @param polyfills An object containing the polyfills 69 | * @param replace If true, replaces the current polyfills instead of mixing in 70 | */ 71 | export function setPolyfills(polyfills: object, replace = false) { 72 | config.polyfills = replace ? polyfills : mix(config.polyfills, polyfills) 73 | } 74 | 75 | /** 76 | * Sets the default method (text, json, …) used to parse the data contained in the response body in case of an HTTP error. 77 | * As with other static methods, it will affect wretch instances created after calling this function. 78 | * 79 | * _Note: if the response Content-Type header is set to "application/json", the body will be parsed as json regardless of the errorType._ 80 | * 81 | * ```js 82 | * import wretch from "wretch" 83 | * 84 | * wretch.errorType("json") 85 | * 86 | * wretch("http://server/which/returns/an/error/with/a/json/body") 87 | * .get() 88 | * .res() 89 | * .catch(error => { 90 | * // error[errorType] (here, json) contains the parsed body 91 | * console.log(error.json) 92 | * }) 93 | * ``` 94 | * 95 | * If null, defaults to "text". 96 | */ 97 | export function setErrorType(errorType: ErrorType) { 98 | config.errorType = errorType 99 | } 100 | 101 | export default config 102 | -------------------------------------------------------------------------------- /src/constants.ts: -------------------------------------------------------------------------------- 1 | export const JSON_MIME = "application/json" 2 | export const CONTENT_TYPE_HEADER = "Content-Type" 3 | export const FETCH_ERROR = Symbol() 4 | export const CATCHER_FALLBACK = Symbol() 5 | -------------------------------------------------------------------------------- /src/core.ts: -------------------------------------------------------------------------------- 1 | import { mix, extractContentType, isLikelyJsonMime } from "./utils.js" 2 | import { JSON_MIME, CONTENT_TYPE_HEADER, CATCHER_FALLBACK } from "./constants.js" 3 | import { resolver } from "./resolver.js" 4 | import config from "./config.js" 5 | import type { Wretch, ErrorType } from "./types.js" 6 | 7 | export const core: Wretch = { 8 | _url: "", 9 | _options: {}, 10 | _config: config, 11 | _catchers: new Map(), 12 | _resolvers: [], 13 | _deferred: [], 14 | _middlewares: [], 15 | _addons: [], 16 | addon(addon) { 17 | return { ...this, _addons: [...this._addons, addon], ...addon.wretch } 18 | }, 19 | errorType(errorType: ErrorType) { 20 | return { 21 | ...this, 22 | _config: { 23 | ...this._config, 24 | errorType 25 | } 26 | } 27 | }, 28 | polyfills(polyfills, replace = false) { 29 | return { 30 | ...this, 31 | _config: { 32 | ...this._config, 33 | polyfills: replace ? polyfills : mix(this._config.polyfills, polyfills) 34 | } 35 | } 36 | }, 37 | url(_url, replace = false) { 38 | if (replace) 39 | return { ...this, _url } 40 | const split = this._url.split("?") 41 | return { 42 | ...this, 43 | _url: split.length > 1 ? 44 | split[0] + _url + "?" + split[1] : 45 | this._url + _url 46 | } 47 | }, 48 | options(options, replace = false) { 49 | return { ...this, _options: replace ? options : mix(this._options, options) } 50 | }, 51 | headers(headerValues) { 52 | const headers = 53 | !headerValues ? {} : 54 | Array.isArray(headerValues) ? Object.fromEntries(headerValues) : 55 | "entries" in headerValues ? Object.fromEntries((headerValues as Headers).entries()) : 56 | headerValues 57 | return { ...this, _options: mix(this._options, { headers }) } 58 | }, 59 | accept(headerValue) { 60 | return this.headers({ Accept: headerValue }) 61 | }, 62 | content(headerValue) { 63 | return this.headers({ [CONTENT_TYPE_HEADER]: headerValue }) 64 | }, 65 | auth(headerValue) { 66 | return this.headers({ Authorization: headerValue }) 67 | }, 68 | catcher(errorId, catcher) { 69 | const newMap = new Map(this._catchers) 70 | newMap.set(errorId, catcher) 71 | return { ...this, _catchers: newMap } 72 | }, 73 | catcherFallback(catcher) { 74 | return this.catcher(CATCHER_FALLBACK, catcher) 75 | }, 76 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 77 | resolve(resolver, clear: boolean = false) { 78 | return { ...this, _resolvers: clear ? [resolver] : [...this._resolvers, resolver] } 79 | }, 80 | defer(callback, clear: boolean = false) { 81 | return { 82 | ...this, 83 | _deferred: clear ? [callback] : [...this._deferred, callback] 84 | } 85 | }, 86 | middlewares(middlewares, clear = false) { 87 | return { 88 | ...this, 89 | _middlewares: clear ? middlewares : [...this._middlewares, ...middlewares] 90 | } 91 | }, 92 | fetch(method: string = this._options.method, url = "", body = null) { 93 | let base = this.url(url).options({ method }) 94 | // "Jsonify" the body if it is an object and if it is likely that the content type targets json. 95 | const contentType = extractContentType(base._options.headers) 96 | const formDataClass = this._config.polyfill("FormData", false) 97 | const jsonify = 98 | typeof body === "object" && 99 | !(formDataClass && body instanceof formDataClass) && 100 | (!base._options.headers || !contentType || isLikelyJsonMime(contentType)) 101 | base = 102 | !body ? base : 103 | jsonify ? base.json(body, contentType) : 104 | base.body(body) 105 | return resolver( 106 | base 107 | ._deferred 108 | .reduce((acc: Wretch, curr) => curr(acc, acc._url, acc._options), base) 109 | ) 110 | }, 111 | get(url = "") { 112 | return this.fetch("GET", url) 113 | }, 114 | delete(url = "") { 115 | return this.fetch("DELETE", url) 116 | }, 117 | put(body, url = "") { 118 | return this.fetch("PUT", url, body) 119 | }, 120 | post(body, url = "") { 121 | return this.fetch("POST", url, body) 122 | }, 123 | patch(body, url = "") { 124 | return this.fetch("PATCH", url, body) 125 | }, 126 | head(url = "") { 127 | return this.fetch("HEAD", url) 128 | }, 129 | opts(url = "") { 130 | return this.fetch("OPTIONS", url) 131 | }, 132 | body(contents) { 133 | return { ...this, _options: { ...this._options, body: contents } } 134 | }, 135 | json(jsObject, contentType) { 136 | const currentContentType = extractContentType(this._options.headers) 137 | return this.content( 138 | contentType || 139 | isLikelyJsonMime(currentContentType) && currentContentType || 140 | JSON_MIME 141 | ).body(JSON.stringify(jsObject)) 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/index.all.ts: -------------------------------------------------------------------------------- 1 | import { setOptions, setErrorType, setPolyfills } from "./config.js" 2 | import { core } from "./core.js" 3 | import * as Addons from "./addons/index.js" 4 | import { WretchError } from "./resolver.js" 5 | 6 | function factory(_url = "", _options = {}) { 7 | return { ...core, _url, _options } 8 | .addon(Addons.abortAddon()) 9 | .addon(Addons.basicAuthAddon) 10 | .addon(Addons.formDataAddon) 11 | .addon(Addons.formUrlAddon) 12 | .addon(Addons.perfsAddon()) 13 | .addon(Addons.queryStringAddon) 14 | .addon(Addons.progressAddon()) 15 | } 16 | 17 | factory["default"] = factory 18 | factory.options = setOptions 19 | factory.errorType = setErrorType 20 | factory.polyfills = setPolyfills 21 | factory.WretchError = WretchError 22 | 23 | export default factory 24 | -------------------------------------------------------------------------------- /src/index.cts: -------------------------------------------------------------------------------- 1 | import factory from "./index.js" 2 | 3 | module.exports = factory.default 4 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { setOptions, setErrorType, setPolyfills } from "./config.js" 2 | import { core } from "./core.js" 3 | import { WretchError } from "./resolver.js" 4 | import type { Wretch, WretchOptions } from "./types.js" 5 | 6 | export type { 7 | Wretch, 8 | Config, 9 | ConfiguredMiddleware, 10 | FetchLike, 11 | Middleware, 12 | WretchResponseChain, 13 | WretchOptions, 14 | WretchError, 15 | WretchErrorCallback, 16 | WretchResponse, 17 | WretchDeferredCallback, 18 | WretchAddon 19 | } from "./types.js" 20 | 21 | /** 22 | * Creates a new wretch instance with a base url and base 23 | * [fetch options](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch). 24 | * 25 | * ```ts 26 | * import wretch from "wretch" 27 | * 28 | * // Reusable instance 29 | * const w = wretch("https://domain.com", { mode: "cors" }) 30 | * ``` 31 | * 32 | * @param _url The base url 33 | * @param _options The base fetch options 34 | * @returns A fresh wretch instance 35 | */ 36 | function factory(_url = "", _options: WretchOptions = {}): Wretch { 37 | return { ...core, _url, _options } 38 | } 39 | 40 | factory["default"] = factory 41 | factory.options = setOptions 42 | factory.errorType = setErrorType 43 | factory.polyfills = setPolyfills 44 | factory.WretchError = WretchError 45 | 46 | export default factory 47 | -------------------------------------------------------------------------------- /src/middleware.ts: -------------------------------------------------------------------------------- 1 | import type { ConfiguredMiddleware, FetchLike } from "./types.js" 2 | 3 | /** 4 | * @private @internal 5 | */ 6 | export const middlewareHelper = (middlewares: ConfiguredMiddleware[]) => (fetchFunction: FetchLike): FetchLike => { 7 | return middlewares.reduceRight((acc, curr) => curr(acc), fetchFunction) || fetchFunction 8 | } 9 | -------------------------------------------------------------------------------- /src/middlewares/dedupe.ts: -------------------------------------------------------------------------------- 1 | import type { ConfiguredMiddleware, WretchOptions } from "../types.js" 2 | 3 | /* Types */ 4 | 5 | export type DedupeSkipFunction = (url: string, opts: WretchOptions) => boolean 6 | export type DedupeKeyFunction = (url: string, opts: WretchOptions) => string 7 | export type DedupeResolverFunction = (response: Response) => Response 8 | export type DedupeOptions = { 9 | skip?: DedupeSkipFunction, 10 | key?: DedupeKeyFunction, 11 | resolver?: DedupeResolverFunction 12 | } 13 | /** 14 | * ## Dedupe middleware 15 | * 16 | * #### Prevents having multiple identical requests on the fly at the same time. 17 | * 18 | * **Options** 19 | * 20 | * - *skip* `(url, opts) => boolean` 21 | * 22 | * > If skip returns true, then the dedupe check is skipped. 23 | * 24 | * - *key* `(url, opts) => string` 25 | * 26 | * > Returns a key that is used to identify the request. 27 | * 28 | * - *resolver* `(response: Response) => Response` 29 | * 30 | * > This function is called when resolving the fetch response from duplicate calls. 31 | * By default it clones the response to allow reading the body from multiple sources. 32 | */ 33 | export type DedupeMiddleware = (options?: DedupeOptions) => ConfiguredMiddleware 34 | 35 | /* Defaults */ 36 | 37 | const defaultSkip: DedupeSkipFunction = (_, opts) => ( 38 | opts.skipDedupe || opts.method !== "GET" 39 | ) 40 | const defaultKey: DedupeKeyFunction = (url: string, opts) => opts.method + "@" + url 41 | const defaultResolver: DedupeResolverFunction = response => response.clone() 42 | 43 | export const dedupe: DedupeMiddleware = ({ skip = defaultSkip, key = defaultKey, resolver = defaultResolver } = {}) => { 44 | 45 | const inflight = new Map() 46 | 47 | return next => (url, opts) => { 48 | 49 | if (skip(url, opts)) { 50 | return next(url, opts) 51 | } 52 | 53 | const _key = key(url, opts) 54 | 55 | if (!inflight.has(_key)) { 56 | inflight.set(_key, []) 57 | } else { 58 | return new Promise((resolve, reject) => { 59 | inflight.get(_key).push([resolve, reject]) 60 | }) 61 | } 62 | 63 | try { 64 | return next(url, opts) 65 | .then(response => { 66 | // Resolve pending promises 67 | inflight.get(_key).forEach(([resolve]) => resolve(resolver(response))) 68 | // Remove the inflight pending promises 69 | inflight.delete(_key) 70 | // Return the original response 71 | return response 72 | }) 73 | .catch(error => { 74 | // Reject pending promises on error 75 | inflight.get(_key).forEach(([, reject]) => reject(error)) 76 | inflight.delete(_key) 77 | throw error 78 | }) 79 | } catch (error) { 80 | inflight.delete(_key) 81 | return Promise.reject(error) 82 | } 83 | 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/middlewares/delay.ts: -------------------------------------------------------------------------------- 1 | import type { ConfiguredMiddleware } from "../types.js" 2 | 3 | /* Types */ 4 | 5 | /** 6 | * ## Delay middleware 7 | * 8 | * ### Delays the request by a specific amount of time. 9 | * 10 | * **Options** 11 | * 12 | * - *time* `milliseconds` 13 | * 14 | * > The request will be delayed by that amount of time. 15 | */ 16 | export type DelayMiddleware = (time: number) => ConfiguredMiddleware 17 | 18 | 19 | export const delay: DelayMiddleware = time => next => (url, opts) => { 20 | return new Promise(res => setTimeout(() => res(next(url, opts)), time)) 21 | } 22 | -------------------------------------------------------------------------------- /src/middlewares/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./delay.js" 2 | export * from "./dedupe.js" 3 | export * from "./throttlingCache.js" 4 | export * from "./retry.js" 5 | -------------------------------------------------------------------------------- /src/resolver.ts: -------------------------------------------------------------------------------- 1 | import { middlewareHelper } from "./middleware.js" 2 | import { mix } from "./utils.js" 3 | import type { Wretch, WretchResponse, WretchResponseChain, WretchError as WretchErrorType } from "./types.js" 4 | import { FETCH_ERROR, CATCHER_FALLBACK } from "./constants.js" 5 | 6 | /** 7 | * This class inheriting from Error is thrown when the fetch response is not "ok". 8 | * It extends Error and adds status, text and body fields. 9 | */ 10 | export class WretchError extends Error implements WretchErrorType { 11 | status: number 12 | response: WretchResponse 13 | url: string 14 | text?: string 15 | json?: any 16 | } 17 | 18 | export const resolver = (wretch: T & Wretch) => { 19 | const sharedState = Object.create(null) 20 | 21 | wretch = wretch._addons.reduce((w, addon) => 22 | addon.beforeRequest && 23 | addon.beforeRequest(w, wretch._options, sharedState) 24 | || w, 25 | wretch) 26 | 27 | const { 28 | _url: url, 29 | _options: opts, 30 | _config: config, 31 | _catchers: _catchers, 32 | _resolvers: resolvers, 33 | _middlewares: middlewares, 34 | _addons: addons 35 | } = wretch 36 | 37 | const catchers = new Map(_catchers) 38 | const finalOptions = mix(config.options, opts) 39 | 40 | // The generated fetch request 41 | let finalUrl = url 42 | const _fetchReq = middlewareHelper(middlewares)((url, options) => { 43 | finalUrl = url 44 | return config.polyfill("fetch")(url, options) 45 | })(url, finalOptions) 46 | // Throws on an http error 47 | const referenceError = new Error() 48 | const throwingPromise: Promise = _fetchReq 49 | .catch(error => { 50 | throw { [FETCH_ERROR]: error } 51 | }) 52 | .then(response => { 53 | if (!response.ok) { 54 | const err = new WretchError() 55 | // Enhance the error object 56 | err["cause"] = referenceError 57 | err.stack = err.stack + "\nCAUSE: " + referenceError.stack 58 | err.response = response 59 | err.status = response.status 60 | err.url = finalUrl 61 | 62 | if (response.type === "opaque") { 63 | throw err 64 | } 65 | 66 | const jsonErrorType = config.errorType === "json" || response.headers.get("Content-Type")?.split(";")[0] === "application/json" 67 | const bodyPromise = 68 | !config.errorType ? Promise.resolve(response.body) : 69 | jsonErrorType ? response.text() : 70 | response[config.errorType]() 71 | 72 | return bodyPromise.then((body: unknown) => { 73 | err.message = typeof body === "string" ? body : response.statusText 74 | if(body) { 75 | if(jsonErrorType && typeof body === "string") { 76 | err.text = body 77 | err.json = JSON.parse(body) 78 | } else { 79 | err[config.errorType] = body 80 | } 81 | } 82 | throw err 83 | }) 84 | } 85 | return response 86 | }) 87 | // Wraps the Promise in order to dispatch the error to a matching catcher 88 | const catchersWrapper = (promise: Promise): Promise => { 89 | return promise.catch(err => { 90 | const fetchErrorFlag = Object.prototype.hasOwnProperty.call(err, FETCH_ERROR) 91 | const error = fetchErrorFlag ? err[FETCH_ERROR] : err 92 | 93 | const catcher = 94 | (error?.status && catchers.get(error.status)) || 95 | catchers.get(error?.name) || ( 96 | fetchErrorFlag && catchers.has(FETCH_ERROR) && catchers.get(FETCH_ERROR) 97 | ) 98 | 99 | if (catcher) 100 | return catcher(error, wretch) 101 | 102 | const catcherFallback = catchers.get(CATCHER_FALLBACK) 103 | if (catcherFallback) 104 | return catcherFallback(error, wretch) 105 | 106 | throw error 107 | }) 108 | } 109 | // Enforces the proper promise type when a body parsing method is called. 110 | type BodyParser = (funName: "json" | "blob" | "formData" | "arrayBuffer" | "text" | null) => (cb?: (type: Type) => Result) => Promise> 111 | const bodyParser: BodyParser = funName => cb => funName ? 112 | // If a callback is provided, then callback with the body result otherwise return the parsed body itself. 113 | catchersWrapper(throwingPromise.then(_ => _ && _[funName]()).then(_ => cb ? cb(_) : _)) : 114 | // No body parsing method - return the response 115 | catchersWrapper(throwingPromise.then(_ => cb ? cb(_ as any) : _)) 116 | 117 | const responseChain: WretchResponseChain = { 118 | _wretchReq: wretch, 119 | _fetchReq, 120 | _sharedState: sharedState, 121 | res: bodyParser(null), 122 | json: bodyParser("json"), 123 | blob: bodyParser("blob"), 124 | formData: bodyParser("formData"), 125 | arrayBuffer: bodyParser("arrayBuffer"), 126 | text: bodyParser("text"), 127 | error(errorId, cb) { 128 | catchers.set(errorId, cb) 129 | return this 130 | }, 131 | badRequest(cb) { return this.error(400, cb) }, 132 | unauthorized(cb) { return this.error(401, cb) }, 133 | forbidden(cb) { return this.error(403, cb) }, 134 | notFound(cb) { return this.error(404, cb) }, 135 | timeout(cb) { return this.error(408, cb) }, 136 | internalError(cb) { return this.error(500, cb) }, 137 | fetchError(cb) { return this.error(FETCH_ERROR, cb) }, 138 | } 139 | 140 | const enhancedResponseChain: R extends undefined ? Chain & WretchResponseChain : R = addons.reduce((chain, addon) => ({ 141 | ...chain, 142 | ...(typeof addon.resolver === "function" ? (addon.resolver as (_: WretchResponseChain) => any)(chain) : addon.resolver) 143 | }), responseChain) 144 | 145 | return resolvers.reduce((chain, r) => r(chain, wretch), enhancedResponseChain) 146 | } 147 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | import { CONTENT_TYPE_HEADER } from "./constants.js" 2 | 3 | export function extractContentType(headers: HeadersInit = {}): string | undefined { 4 | const normalizedHeaders = headers instanceof Array ? Object.fromEntries(headers) : headers 5 | return Object.entries(normalizedHeaders).find(([k]) => 6 | k.toLowerCase() === CONTENT_TYPE_HEADER.toLowerCase() 7 | )?.[1] 8 | } 9 | 10 | export function isLikelyJsonMime(value: string): boolean { 11 | return /^application\/.*json.*/.test(value) 12 | } 13 | 14 | export const mix = function (one: object, two: object, mergeArrays: boolean = false) { 15 | return Object.entries(two).reduce((acc, [key, newValue]) => { 16 | const value = one[key] 17 | if (Array.isArray(value) && Array.isArray(newValue)) { 18 | acc[key] = mergeArrays ? [...value, ...newValue] : newValue 19 | } else if (typeof value === "object" && typeof newValue === "object") { 20 | acc[key] = mix(value, newValue, mergeArrays) 21 | } else { 22 | acc[key] = newValue 23 | } 24 | 25 | return acc 26 | }, { ...one }) 27 | } 28 | -------------------------------------------------------------------------------- /test/assets/duck.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/elbywan/wretch/bf1dfe8fe532fadc6b6391fb678a785c3efc9b4b/test/assets/duck.jpg -------------------------------------------------------------------------------- /test/node/middlewares/dedupe.spec.ts: -------------------------------------------------------------------------------- 1 | import * as http from "http" 2 | import wretch, { WretchOptions } from "../../../src" 3 | import { dedupe } from "../../../src/middlewares" 4 | import { mock } from "./mock" 5 | 6 | export default describe("Dedupe Middleware", () => { 7 | const PORT = 0 8 | let server: http.Server | null = null 9 | let logs: any[] = [] 10 | 11 | const log = (url: string, options: WretchOptions) => { 12 | logs.push([url, options.method]) 13 | } 14 | 15 | const baseAddress = () => { 16 | const { address, port } = (server as any).address() 17 | return "http://" + address + ":" + port 18 | } 19 | 20 | beforeAll(done => { 21 | server = http.createServer((req, res) => { 22 | req.pipe(res) 23 | }) 24 | server.listen(PORT, "127.0.0.1") 25 | server.once("listening", () => { 26 | done() 27 | }) 28 | server.once("error", () => { 29 | done() 30 | }) 31 | }) 32 | 33 | afterAll(() => { 34 | server?.close() 35 | }) 36 | 37 | beforeEach(() => { 38 | logs = [] 39 | }) 40 | 41 | it("should prevent sending multiple requests", async () => { 42 | const w = wretch(baseAddress()).polyfills({ fetch: mock(log) }).middlewares([dedupe()]) 43 | const results = await Promise.all([ 44 | w.get("/one").res(), 45 | w.get("/one").res(), 46 | w.get("/one").res(), 47 | w.get("/two").res(), 48 | w.get("/two").res(), 49 | w.get("/three").res(), 50 | w.post("body", "/one").res(), 51 | w.post("body", "/one").res(), 52 | ]) 53 | 54 | expect(logs).toEqual([ 55 | [baseAddress() + "/one", "GET"], 56 | [baseAddress() + "/two", "GET"], 57 | [baseAddress() + "/three", "GET"], 58 | [baseAddress() + "/one", "POST"], 59 | [baseAddress() + "/one", "POST"] 60 | ]) 61 | 62 | results.forEach((result, i) => { 63 | expect(result).toMatchObject({ 64 | url: baseAddress() + "/" + ((i < 3 || i > 5) ? "one" : i < 5 ? "two" : "three"), 65 | status: 200, 66 | statusText: "OK", 67 | }) 68 | }) 69 | }) 70 | 71 | it("should skip some requests", async () => { 72 | const w = wretch(baseAddress()).polyfills({ fetch: mock(log) }).middlewares([dedupe({ 73 | skip: (url, options) => { return options.skip || url.endsWith("/toto") } 74 | })]) 75 | await Promise.all([ 76 | w.get("/one").res(), 77 | w.get("/one").res(), 78 | w.get("/one").res(), 79 | w.options({ skip: true }).get("/one").res(), 80 | w.get("/toto").res(), 81 | w.get("/toto").res() 82 | ]) 83 | 84 | expect(logs).toEqual([ 85 | [baseAddress() + "/one", "GET"], 86 | [baseAddress() + "/one", "GET"], 87 | [baseAddress() + "/toto", "GET"], 88 | [baseAddress() + "/toto", "GET"], 89 | ]) 90 | }) 91 | 92 | it("should key requests", async () => { 93 | const w = wretch(baseAddress()).polyfills({ fetch: mock(log) }).middlewares([dedupe({ 94 | key: () => { return "/same-key" } 95 | })]) 96 | 97 | const results = await Promise.all([ 98 | w.get("/one").res(), 99 | w.get("/two").res(), 100 | w.get("/three").res() 101 | ]) 102 | 103 | expect(logs).toEqual([ 104 | [baseAddress() + "/one", "GET"] 105 | ]) 106 | 107 | results.forEach(result => { 108 | expect(result).toMatchObject({ 109 | url: baseAddress() + "/one", 110 | status: 200, 111 | statusText: "OK", 112 | }) 113 | }) 114 | }) 115 | 116 | it("should allow custom resolvers", async () => { 117 | const w = wretch(baseAddress()).polyfills({ fetch: mock(log) }).middlewares([dedupe({ 118 | resolver: res => res 119 | })]) 120 | 121 | const results = await Promise.all([ 122 | w.get("/one").res(), 123 | w.get("/one").res(), 124 | w.get("/one").res() 125 | ]) 126 | 127 | expect(results[0]).toStrictEqual(results[1]) 128 | expect(results[0]).toStrictEqual(results[2]) 129 | }) 130 | }) 131 | -------------------------------------------------------------------------------- /test/node/middlewares/delay.spec.ts: -------------------------------------------------------------------------------- 1 | import wretch from "../../../src" 2 | import { delay } from "../../../src/middlewares" 3 | 4 | export default describe("Delay Middleware", () => { 5 | it("should delay requests", async () => { 6 | let before = 0 7 | let after = 0 8 | await wretch("").polyfills({ fetch: () => Promise.resolve({ ok: true }) }).middlewares([ 9 | next => (url, options) => { 10 | before = new Date().getTime() 11 | return next(url, options).then(response => { 12 | after = new Date().getTime() 13 | return response 14 | }) 15 | }, 16 | delay(1000) 17 | ]).get().res() 18 | expect(after - before).toBeGreaterThanOrEqual(1000) 19 | }) 20 | }) 21 | -------------------------------------------------------------------------------- /test/node/middlewares/mock.ts: -------------------------------------------------------------------------------- 1 | import nodeFetch from "node-fetch" 2 | 3 | export const mock = (cb, fetch = nodeFetch) => { 4 | return (url, options) => { 5 | cb(url, options) 6 | return fetch(url, options) 7 | } 8 | } -------------------------------------------------------------------------------- /test/resolver.cjs: -------------------------------------------------------------------------------- 1 | const path = require("path") 2 | const srcDir = path.resolve(__dirname, "..", "src") 3 | 4 | module.exports = (path, options) => { 5 | if (options.basedir.startsWith(srcDir) && path.endsWith(".js")) { 6 | return options.defaultResolver(path.substring(0, path.length - 3) + ".ts", options); 7 | } 8 | return options.defaultResolver(path, options); 9 | }; -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2015", 4 | "lib": [ 5 | "es2015", 6 | "dom", 7 | "dom.iterable" 8 | ], 9 | "module": "commonjs", 10 | "noImplicitAny": false, 11 | "sourceMap": true, 12 | "allowJs": true, 13 | "types": [ 14 | "node", 15 | "jest" 16 | ], 17 | }, 18 | "include": [ 19 | "./node/**/*.spec.ts" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "CommonJS", 5 | "outDir": "dist/cjs", 6 | "declarationDir": "dist/cjs" 7 | }, 8 | "exclude": [] 9 | } 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2018", 4 | "lib": ["es2020", "dom", "dom.iterable"], 5 | "module": "es2015", 6 | "outDir": "dist", 7 | "declaration": true, 8 | "declarationDir": "dist", 9 | "noImplicitAny": false, 10 | "sourceMap": true, 11 | "inlineSources": true, 12 | "moduleResolution": "node", 13 | "isolatedModules": true 14 | }, 15 | "include": ["src/**/*"], 16 | "exclude": ["src/**/index.cts"] 17 | } 18 | --------------------------------------------------------------------------------