├── .editorconfig ├── .github └── workflows │ ├── ci.yml │ └── publish-latest.yml ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── package.json ├── rollup.config.js ├── src ├── __tests__ │ ├── test-cacheAdapterEnhancer.ts │ ├── test-retryAdapterEnhancer.ts │ └── test-throttleAdapterEnhancer.ts ├── cacheAdapterEnhancer.ts ├── index.ts ├── retryAdapterEnhancer.ts ├── throttleAdapterEnhancer.ts └── utils │ ├── __tests__ │ ├── test-buildSortedURL.ts │ └── test-isCacheLike.ts │ ├── buildSortedURL.ts │ └── isCacheLike.ts ├── tsconfig.esm.json ├── tsconfig.json └── tslint.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [**] 5 | # Unix-style newlines with a newline ending every file 6 | end_of_line = lf 7 | insert_final_newline = true 8 | 9 | trim_trailing_whitespace = true 10 | 11 | charset = utf-8 12 | 13 | # use tab as indent style 14 | indent_style = tab 15 | 16 | [*.md] 17 | trim_trailing_whitespace = false 18 | 19 | [{package.json,*.babelrc,.eslintrc,.esdocrc}] 20 | indent_style = space 21 | indent_size = 2 22 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - master 8 | - next 9 | - 1.x 10 | 11 | jobs: 12 | check: 13 | runs-on: ubuntu-latest 14 | 15 | strategy: 16 | matrix: 17 | node-version: [14.x, 16.x] 18 | 19 | steps: 20 | - uses: actions/checkout@v2 21 | 22 | - name: Use Node.js ${{ matrix.node-version }} 23 | uses: actions/setup-node@v2 24 | with: 25 | node-version: ${{ matrix.node-version }} 26 | 27 | - name: Get yarn cache directory path 28 | id: yarn-cache-dir-path 29 | run: echo "::set-output name=dir::$(yarn cache dir)" 30 | 31 | - uses: actions/cache@v2 32 | id: yarn-cache 33 | with: 34 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 35 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 36 | restore-keys: | 37 | ${{ runner.os }}-yarn- 38 | - run: yarn 39 | - run: yarn ci 40 | -------------------------------------------------------------------------------- /.github/workflows/publish-latest.yml: -------------------------------------------------------------------------------- 1 | name: Publish Latest Version 2 | 3 | on: 4 | push: 5 | tags: 6 | - v3.* 7 | 8 | jobs: 9 | publish: 10 | 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - uses: actions/setup-node@v2 17 | with: 18 | node-version: '14.x' 19 | check-latest: true 20 | registry-url: 'https://registry.npmjs.org' 21 | 22 | - run: yarn 23 | - run: yarn publish --tag latest 24 | env: 25 | NODE_AUTH_TOKEN: ${{ secrets.NPM_AUTH_TOKEN }} 26 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .DS_Store 3 | .nyc_output/ 4 | coverage.lcov 5 | coverage/ 6 | node_modules/ 7 | dist/ 8 | esm/ 9 | lib/ 10 | yarn.lock 11 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | __tests__ 2 | *.js.map 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 axios-extensions 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # axios-extensions 2 | 3 | [![npm version](https://img.shields.io/npm/v/axios-extensions.svg?style=flat-square)](https://www.npmjs.com/package/axios-extensions) 4 | [![coverage](https://img.shields.io/codecov/c/github/kuitos/axios-extensions.svg?style=flat-square)](https://codecov.io/gh/kuitos/axios-extensions) 5 | [![npm downloads](https://img.shields.io/npm/dt/axios-extensions.svg?style=flat-square)](https://www.npmjs.com/package/axios-extensions) 6 | [![Build Status](https://img.shields.io/github/actions/workflow/status/kuitos/axios-extensions/ci.yml?branch=master&style=flat-square)](https://github.com/kuitos/axios-extensions/actions/workflows/ci.yml) 7 | 8 | A non-invasive, simple, reliable collection of axios extension 9 | 10 | ## Extension List 11 | *v3.x has a lot of api changes, if you are looking for v2.x doc, see [here](https://github.com/kuitos/axios-extensions/tree/v2.0.3)* 12 | 13 | *Not working with axios v0.19.0 as its custom config bug, See https://github.com/axios/axios/pull/2207.* 14 | 15 | * [cacheAdapterEnhancer](#cacheadapterenhancer) makes request cacheable 16 | * [throttleAdapterEnhancer](#throttleadapterenhancer) makes GET requests throttled automatically 17 | * [retryAdapterEnhancer](#retryadapterenhancer) makes request retry with special times while it failed 18 | 19 | ## Installing 20 | ```bash 21 | npm i axios-extensions -S 22 | ``` 23 | or 24 | ```bash 25 | yarn add axios-extensions 26 | ``` 27 | 28 | or 29 | ```html 30 | // exposed as window['axios-extensions'] 31 | 32 | ``` 33 | 34 | ## Usage 35 | 36 | ```javascript 37 | import axios from 'axios'; 38 | import { cacheAdapterEnhancer, throttleAdapterEnhancer } from 'axios-extensions'; 39 | 40 | // enhance the original axios adapter with throttle and cache enhancer 41 | const http = axios.create({ 42 | baseURL: '/', 43 | headers: { 'Cache-Control': 'no-cache' }, 44 | adapter: throttleAdapterEnhancer(cacheAdapterEnhancer(axios.defaults.adapter)) 45 | }); 46 | ``` 47 | 48 | ### Enable Logging 49 | 50 | It is highly recommended to enable the request logging recorder in development environment(disabled by default). 51 | 52 | #### browser (webpack) 53 | ```js 54 | new webpack.DefinePlugin({ 55 | 'process.env.LOGGER_LEVEL': JSON.stringify('info') 56 | }) 57 | ``` 58 | #### node 59 | ```json 60 | // package.json 61 | "scripts": { 62 | "start": "cross-env LOGGER_LEVEL=info node server.js" 63 | } 64 | ``` 65 | 66 | ## API 67 | 68 | ### cacheAdapterEnhancer 69 | 70 | > Makes axios cacheable 71 | 72 | ```typescript 73 | cacheAdapterEnhancer(adapter: AxiosAdapter, options: Options): AxiosAdapter 74 | ``` 75 | 76 | Where `adapter` is an axios adapter which following the [axios adapter standard](https://github.com/axios/axios/blob/master/lib/adapters/README.md), `options` is an optional that configuring caching: 77 | 78 | | Param | Type | Default value | Description | 79 | | ---------------- | ---------------------------------------- | ------------------------------------------------------------ | ---- | 80 | | enabledByDefault | boolean | true | Enables cache for all requests without explicit definition in request config (e.g. `cache: true`) | 81 | | cacheFlag | string | 'cache' | Configures key (flag) for explicit definition of cache usage in axios request | 82 | | defaultCache | CacheLike |
new LRUCache({ maxAge: FIVE_MINUTES, max: 100 })
| a CacheLike instance that will be used for storing requests by default, except you define a custom Cache with your request config | 83 | 84 | `cacheAdapterEnhancer` enhances the given adapter and returns a new cacheable adapter back, so you can compose it with any other enhancers, e.g. `throttleAdapterEnhancer`. 85 | 86 | #### basic usage 87 | 88 | ```javascript 89 | import axios from 'axios'; 90 | import { cacheAdapterEnhancer } from 'axios-extensions'; 91 | 92 | const http = axios.create({ 93 | baseURL: '/', 94 | headers: { 'Cache-Control': 'no-cache' }, 95 | // cache will be enabled by default 96 | adapter: cacheAdapterEnhancer(axios.defaults.adapter) 97 | }); 98 | 99 | http.get('/users'); // make real http request 100 | http.get('/users'); // use the response from the cache of previous request, without real http request made 101 | http.get('/users', { cache: false }); // disable cache manually and the the real http request invoked 102 | ``` 103 | 104 | #### custom cache flag 105 | 106 | ```javascript 107 | const http = axios.create({ 108 | baseURL: '/', 109 | headers: { 'Cache-Control': 'no-cache' }, 110 | // disable the default cache and set the cache flag 111 | adapter: cacheAdapterEnhancer(axios.defaults.adapter, { enabledByDefault: false, cacheFlag: 'useCache'}) 112 | }); 113 | 114 | http.get('/users'); // default cache was disabled and then the real http request invoked 115 | http.get('/users', { useCache: true }); // make the request cacheable(real http request made due to first request invoke) 116 | http.get('/users', { useCache: true }); // use the response cache from previous request 117 | ``` 118 | 119 | ##### custom cache typing 120 | 121 | Note that if you are using custom cache flag and typescript, you may need to add the typing declaration like below: 122 | 123 | ```ts 124 | import { ICacheLike } from 'axios-extensions'; 125 | declare module 'axios' { 126 | interface AxiosRequestConfig { 127 | // if your cacheFlag was setting to 'useCache' 128 | useCache?: boolean | ICacheLike; 129 | } 130 | } 131 | ``` 132 | 133 | #### more advanced 134 | 135 | Besides configuring the request through the `cacheAdapterEnhancer`, we can enjoy more advanced features via configuring every individual request. 136 | 137 | ```js 138 | import axios from 'axios'; 139 | import { cacheAdapterEnhancer, Cache } from 'axios-extensions'; 140 | 141 | const http = axios.create({ 142 | baseURL: '/', 143 | headers: { 'Cache-Control': 'no-cache' }, 144 | // disable the default cache 145 | adapter: cacheAdapterEnhancer(axios.defaults.adapter, { enabledByDefault: false }) 146 | }); 147 | 148 | http.get('/users', { cache: true }); // make the request cacheable(real http request made due to first request invoke) 149 | 150 | // define a cache manually 151 | const cacheA = new Cache(); 152 | // or a cache-like instance 153 | const cacheB = { get() {/*...*/}, set() {/*...*/}, del() {/*...*/} }; 154 | 155 | // two actual request will be made due to the different cache 156 | http.get('/users', { cache: cacheA }); 157 | http.get('/users', { cache: cacheB }); 158 | 159 | // a actual request made and cached due to force update configured 160 | http.get('/users', { cache: cacheA, forceUpdate: true }); 161 | ``` 162 | 163 | *Note: If you are using typescript, do not forget to enable `"esModuleInterop": true` and `"allowSyntheticDefaultImports": true` for better development experience.* 164 | 165 | ### throttleAdapterEnhancer 166 | 167 | > Throttle GET requests most once per threshold milliseconds 168 | 169 | ```ts 170 | throttleAdapterEnhancer(adapter: AxiosAdapter, options: Options): AxiosAdapter 171 | ``` 172 | 173 | Where `adapter` is an axios adapter which following the [axios adapter standard](https://github.com/axios/axios/blob/master/lib/adapters/README.md), `options` is an optional object that configuring throttling: 174 | 175 | | Param | Type |Default value | Description | 176 | | --------- | ---- |--------------------------- | ------------------------------------------------------------ | 177 | | threshold | number |1000 | The number of milliseconds to throttle request invocations to | 178 | | cache | CacheLike |
new LRUCache({ max: 10 })
| CacheLike instance that will be used for storing throttled requests | 179 | 180 | Basically we recommend using the `throttleAdapterEnhancer` with `cacheAdapterEnhancer` together for the maximum caching benefits. 181 | Note that POST and other methods besides GET are not affected. 182 | 183 | ```js 184 | throttleAdapterEnhancer(cacheAdapterEnhancer(axios.defaults.adapter)) 185 | ``` 186 | 187 | Check [David Corbacho's article](https://css-tricks.com/debouncing-throttling-explained-examples/) to learn more details about throttle and how it differs from debounce. 188 | 189 | #### basic usage 190 | 191 | ```js 192 | import axios from 'axios'; 193 | import { throttleAdapterEnhancer } from 'axios-extensions'; 194 | 195 | const http = axios.create({ 196 | baseURL: '/', 197 | headers: { 'Cache-Control': 'no-cache' }, 198 | adapter: throttleAdapterEnhancer(axios.defaults.adapter, { threshold: 2 * 1000 }) 199 | }); 200 | 201 | http.get('/users'); // make real http request 202 | http.get('/users'); // responsed from the cache 203 | http.get('/users'); // responsed from the cache 204 | 205 | setTimeout(() => { 206 | http.get('/users'); // after 2s, the real request makes again 207 | }, 2 * 1000); 208 | ``` 209 | 210 | ### retryAdapterEnhancer 211 | 212 | > Retry the failed request with special times 213 | 214 | ```ts 215 | retryAdapterEnhancer(adapter: AxiosAdapter, options: Options): AxiosAdapter 216 | ``` 217 | 218 | Where `adapter` is an axios adapter which following the [axios adapter standard](https://github.com/axios/axios/blob/master/lib/adapters/README.md), `options` is an optional that configuring caching: 219 | | Param | Type | Default value | Description | 220 | | ---------------- | ---------------------------------------- | ------------------------------------------------------------ | ---- | 221 | | times | number | 2 | Set the retry times for failed request globally. | 222 | 223 | #### basic usage 224 | 225 | ```ts 226 | import axios from 'axios'; 227 | import { retryAdapterEnhancer } from 'axios-extensions'; 228 | 229 | const http = axios.create({ 230 | baseURL: '/', 231 | headers: { 'Cache-Control': 'no-cache' }, 232 | adapter: retryAdapterEnhancer(axios.defaults.adapter) 233 | }); 234 | 235 | // this request will retry two times if it failed 236 | http.get('/users'); 237 | 238 | // you could also set the retry times for a special request 239 | http.get('/special', { retryTimes: 3 }); 240 | ``` 241 | 242 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "axios-extensions", 3 | "version": "3.1.7", 4 | "description": "make axios great again", 5 | "homepage": "https://github.com/kuitos/axios-extensions", 6 | "repository": "https://github.com/kuitos/axios-extensions.git", 7 | "license": "MIT", 8 | "author": "kuitos", 9 | "keywords": [ 10 | "axios", 11 | "cache", 12 | "extensions", 13 | "adapter" 14 | ], 15 | "main": "./lib/index.js", 16 | "module": "./esm/index.js", 17 | "types": "./esm/index.d.ts", 18 | "sideEffects": false, 19 | "scripts": { 20 | "build": "rm -fr dist && npm run tsc && npm run dist", 21 | "tsc": "npm run tsc:cjs & npm run tsc:esm", 22 | "tsc:cjs": "rm -fr lib && tsc", 23 | "tsc:esm": "rm -fr esm && tsc -p tsconfig.esm.json", 24 | "dist": "rm -fr dist && rollup -c rollup.config.js", 25 | "start": "tsc -w && ava -w", 26 | "prepush": "npm run lint", 27 | "prepublishOnly": "npm run build", 28 | "release": "np --no-cleanup --yolo --no-publish", 29 | "test": "npm run tsc:cjs && nyc ava -v", 30 | "test:pure": "tsc && ava", 31 | "report": "nyc report --reporter=html", 32 | "codecov": "nyc report --reporter=text-lcov > coverage.lcov && codecov", 33 | "coverageCheck": "nyc --check-coverage --lines 80 --functions 80 --branches 80 npm run test:pure", 34 | "lint": "tslint 'src/**/*.ts' & npm run coverageCheck", 35 | "ci": "npm run lint && npm run codecov" 36 | }, 37 | "files": [ 38 | "esm", 39 | "lib", 40 | "dist", 41 | "src" 42 | ], 43 | "dependencies": { 44 | "lru-cache": "^7.14.0", 45 | "tslib": "^2.1.0", 46 | "util": "^0.12.3" 47 | }, 48 | "peerDependencies": { 49 | "axios": "*" 50 | }, 51 | "devDependencies": { 52 | "@types/node": "^14.14.22", 53 | "@types/sinon": "^9.0.10", 54 | "ava": "^3.15.0", 55 | "axios": ">=0.21.1", 56 | "codecov": "^3.8.1", 57 | "husky": "^4.3.8", 58 | "np": "^7.2.0", 59 | "nyc": "^15.1.0", 60 | "rollup": "^2.37.1", 61 | "rollup-plugin-commonjs": "^10.0.0", 62 | "rollup-plugin-node-builtins": "^2.1.2", 63 | "rollup-plugin-node-resolve": "^5.0.0", 64 | "rollup-plugin-terser": "^7.0.2", 65 | "rollup-plugin-uglify": "^6.0.4", 66 | "sinon": "^9.2.3", 67 | "tslint": "^5.20.1", 68 | "tslint-eslint-rules": "^5.4.0", 69 | "typescript": "^4.1.3" 70 | }, 71 | "ava": { 72 | "files": [ 73 | "lib/**/__tests__/**/*.js" 74 | ] 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Kuitos 3 | * @since 2019-05-27 4 | */ 5 | 6 | const resolve = require('rollup-plugin-node-resolve'); 7 | const commonjs = require('rollup-plugin-commonjs'); 8 | const builtin = require('rollup-plugin-node-builtins'); 9 | const terser = require('rollup-plugin-terser').terser; 10 | 11 | function genConfig(minimize = false) { 12 | 13 | return { 14 | input: './esm/index.js', 15 | output: { 16 | name: 'axios-extensions', 17 | file: minimize ? './dist/axios-extensions.min.js' : './dist/axios-extensions.js', 18 | format: 'umd', 19 | sourcemap: true, 20 | }, 21 | external: ['axios'], 22 | plugins: [ 23 | resolve(), 24 | commonjs(), 25 | builtin(), 26 | minimize ? terser() : void 0, 27 | ], 28 | }; 29 | } 30 | 31 | module.exports = [ 32 | genConfig(), 33 | genConfig(true), 34 | ]; 35 | -------------------------------------------------------------------------------- /src/__tests__/test-cacheAdapterEnhancer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Kuitos 3 | * @homepage https://github.com/kuitos/ 4 | * @since 2017-10-16 5 | */ 6 | 7 | import test from 'ava'; 8 | import axios, { AxiosPromise } from 'axios'; 9 | import LRUCache from 'lru-cache'; 10 | import { spy } from 'sinon'; 11 | 12 | import cacheAdapterEnhancer from '../cacheAdapterEnhancer'; 13 | 14 | // mock the actual request 15 | const genMockAdapter = (cb: any) => (config: any) => { 16 | cb(); 17 | if (config.error) { 18 | return Promise.reject(config); 19 | } 20 | return Promise.resolve(config); 21 | }; 22 | 23 | test('cache adapter should cache request without noCacheFlag', async t => { 24 | 25 | const adapterCb = spy(); 26 | const mockedAdapter = genMockAdapter(adapterCb); 27 | const http = axios.create({ 28 | adapter: cacheAdapterEnhancer(mockedAdapter, { enabledByDefault: true }), 29 | }); 30 | 31 | const onSuccess = spy(); 32 | const promises = []; 33 | 34 | for (let i = 0; i < 5; i++) { 35 | promises.push(http.get('/users').then(onSuccess)); 36 | } 37 | 38 | await Promise.all(promises); 39 | 40 | t.is(onSuccess.callCount, 5); 41 | t.is(adapterCb.callCount, 1); 42 | t.is(adapterCb.calledBefore(onSuccess), true); 43 | 44 | await http.get('/users', { params: { name: 'kuitos' } }).then(onSuccess); 45 | t.is(onSuccess.callCount, 6); 46 | t.is(adapterCb.callCount, 2); 47 | 48 | await http.get('/users', { params: { name: 'kuitos' }, cache: true } as any).then(onSuccess); 49 | t.is(onSuccess.callCount, 7); 50 | t.is(adapterCb.callCount, 2); 51 | 52 | }); 53 | 54 | test('cache adapter shouldn\'t cache request with noCacheFlag', async t => { 55 | 56 | const adapterCb = spy(); 57 | const mockedAdapter = genMockAdapter(adapterCb); 58 | const http = axios.create({ 59 | adapter: cacheAdapterEnhancer(mockedAdapter, { enabledByDefault: true, cacheFlag: 'cache' }), 60 | }); 61 | 62 | const onSuccess = spy(); 63 | await Promise.all([ 64 | http.get('/users', { cache: false } as any).then(onSuccess), 65 | http.get('/users', { cache: false } as any).then(onSuccess), 66 | ]); 67 | t.is(onSuccess.callCount, 2); 68 | t.is(adapterCb.callCount, 2); 69 | 70 | }); 71 | 72 | test('cache will be removed when request error', async t => { 73 | 74 | const adapterCb = spy(); 75 | const mockedAdapter = genMockAdapter(adapterCb); 76 | const http = axios.create({ 77 | adapter: cacheAdapterEnhancer(mockedAdapter, { enabledByDefault: true }), 78 | }); 79 | 80 | const onSuccess = spy(); 81 | const onError = spy(); 82 | await Promise.all([ 83 | http.get('/users', { error: true } as any).then(onSuccess, onError), 84 | http.get('/users').then(onSuccess, onError), 85 | ]); 86 | // as the previous uses invocation failed, the following users request will respond with the rejected promise 87 | t.is(onSuccess.callCount, 0); 88 | t.is(onError.callCount, 2); 89 | t.is(adapterCb.callCount, 1); 90 | 91 | await Promise.all([ 92 | http.get('/users').then(onSuccess, onError), 93 | http.get('/users').then(onSuccess, onError), 94 | ]); 95 | t.is(onSuccess.callCount, 2); 96 | t.is(adapterCb.callCount, 2); 97 | 98 | }); 99 | 100 | test('disable default cache switcher', async t => { 101 | 102 | const adapterCb = spy(); 103 | const mockedAdapter = genMockAdapter(adapterCb); 104 | const http = axios.create({ 105 | adapter: cacheAdapterEnhancer(mockedAdapter), 106 | }); 107 | 108 | const onSuccess = spy(); 109 | await Promise.all([ 110 | http.get('/users').then(onSuccess), 111 | http.get('/users').then(onSuccess), 112 | http.get('/users', { cache: false } as any).then(onSuccess), 113 | ]); 114 | t.is(onSuccess.callCount, 3); 115 | t.is(adapterCb.callCount, 2); 116 | 117 | }); 118 | 119 | test('request will refresh the cache with forceUpdate config', async t => { 120 | 121 | const adapterCb = spy(); 122 | const mockedAdapter = genMockAdapter(adapterCb); 123 | const cache = new LRUCache({ max: 100 }); 124 | const http = axios.create({ 125 | adapter: cacheAdapterEnhancer(mockedAdapter, { enabledByDefault: true, cacheFlag: 'cache', defaultCache: cache }), 126 | }); 127 | 128 | const onSuccess = spy(); 129 | await http.get('/users').then(onSuccess); 130 | const responed1 = await cache.get('/users') as any; 131 | await http.get('/users', { forceUpdate: true } as any).then(onSuccess); 132 | const responed2 = await cache.get('/users') as any; 133 | t.is(adapterCb.callCount, 2); 134 | t.is(onSuccess.callCount, 2); 135 | 136 | if (responed1) { 137 | t.is(responed1.url, '/users'); 138 | t.is(responed1.url, responed2.url); 139 | } 140 | 141 | t.not(responed1, responed2); 142 | 143 | }); 144 | 145 | test('use a custom cache with request individual config', async t => { 146 | 147 | const adapterCb = spy(); 148 | const mockedAdapter = genMockAdapter(adapterCb); 149 | const http = axios.create({ 150 | adapter: cacheAdapterEnhancer(mockedAdapter), 151 | }); 152 | 153 | const cache1 = new LRUCache({ max: 100 }); 154 | const cache2 = new LRUCache({ max: 100 }); 155 | await Promise.all([http.get('/users', { cache: cache1 } as any), http.get('/users', { cache: cache2 } as any)]); 156 | t.is(adapterCb.callCount, 2); 157 | 158 | cache2.clear(); 159 | await Promise.all([http.get('/users', { cache: cache1 } as any), http.get('/users', { cache: cache2 } as any)]); 160 | 161 | t.is(adapterCb.callCount, 3); 162 | }); 163 | -------------------------------------------------------------------------------- /src/__tests__/test-retryAdapterEnhancer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Kuitos 3 | * @since 2020-02-18 4 | */ 5 | 6 | import test from 'ava'; 7 | import axios from 'axios'; 8 | import { spy } from 'sinon'; 9 | import retryAdapterEnhancer from '../retryAdapterEnhancer'; 10 | 11 | test('should retry the request with special times while request failed', async (t) => { 12 | 13 | const times = 3; 14 | const spyFn = spy(); 15 | const mockedAdapter = (config: any) => { 16 | spyFn(); 17 | if (spyFn.callCount === times + 1) { 18 | return Promise.resolve(config); 19 | } 20 | return Promise.reject(config); 21 | }; 22 | const http = axios.create({ 23 | adapter: retryAdapterEnhancer(mockedAdapter, { times }), 24 | }); 25 | 26 | await http.get('/test'); 27 | 28 | t.is(spyFn.callCount, times + 1); 29 | }); 30 | 31 | test('should return the result immediately while the request succeed', async (t) => { 32 | const spyFn = spy(); 33 | const mockedAdapter = (config: any) => { 34 | spyFn(); 35 | if (spyFn.calledTwice) { 36 | return Promise.resolve(config); 37 | } 38 | 39 | return Promise.reject(config); 40 | }; 41 | const http = axios.create({ 42 | adapter: retryAdapterEnhancer(mockedAdapter), 43 | }); 44 | 45 | await http.get('/test'); 46 | 47 | t.truthy(spyFn.calledTwice); 48 | }); 49 | 50 | test('should throw an exception while request still failed after retry', async (t) => { 51 | 52 | const defaultTimes = 2; 53 | const spyFn = spy(); 54 | const mockedAdapter = (config: any) => { 55 | spyFn(); 56 | return Promise.reject(config); 57 | }; 58 | const http = axios.create({ 59 | adapter: retryAdapterEnhancer(mockedAdapter), 60 | }); 61 | 62 | try { 63 | await http.get('/test'); 64 | } catch (e: any) { 65 | t.is(e.url, '/test'); 66 | t.is(spyFn.callCount, defaultTimes + 1); 67 | } 68 | }); 69 | 70 | test('should retry with special times for the custom config request', async (t) => { 71 | 72 | const spyFn = spy(); 73 | const mockedAdapter = (config: any) => { 74 | spyFn(); 75 | return Promise.reject(config); 76 | }; 77 | const http = axios.create({ 78 | adapter: retryAdapterEnhancer(mockedAdapter, { times: 2 }), 79 | }); 80 | 81 | const customRetryTimes = 4; 82 | try { 83 | await http.get('/test', { retryTimes: customRetryTimes }); 84 | } catch (e: any) { 85 | t.is(e.url, '/test'); 86 | t.is(spyFn.callCount, customRetryTimes + 1); 87 | } 88 | }); 89 | -------------------------------------------------------------------------------- /src/__tests__/test-throttleAdapterEnhancer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Kuitos 3 | * @homepage https://github.com/kuitos/ 4 | * @since 2017-10-16 5 | */ 6 | 7 | import test from 'ava'; 8 | import axios from 'axios'; 9 | import LRUCache from 'lru-cache'; 10 | import { spy } from 'sinon'; 11 | 12 | import throttleAdapterEnhancer, { RecordedCache } from '../throttleAdapterEnhancer'; 13 | 14 | const genMockAdapter = (cb: any) => (config: any) => { 15 | cb(); 16 | if (config.error) { 17 | return Promise.reject(config); 18 | } 19 | return Promise.resolve(config); 20 | }; 21 | 22 | test('throttle adapter should cache request in a threshold seconds', async t => { 23 | 24 | const threshold = 1000; 25 | const adapterCb = spy(); 26 | const mockedAdapter = genMockAdapter(adapterCb); 27 | const http = axios.create({ 28 | adapter: throttleAdapterEnhancer(mockedAdapter, { threshold }), 29 | }); 30 | 31 | const onSuccess = spy(); 32 | const promises = []; 33 | 34 | const start = Date.now(); 35 | for (let i = 0; i < 5; i++) { 36 | promises.push(http.get('/users').then(onSuccess)); 37 | } 38 | 39 | await Promise.all(promises); 40 | const end = Date.now(); 41 | t.is(onSuccess.callCount, 5); 42 | t.is(adapterCb.callCount, 1); 43 | t.is(adapterCb.calledBefore(onSuccess), true); 44 | t.is(end - start < threshold, true); 45 | 46 | await new Promise(r => setTimeout(r, threshold)); 47 | await Promise.all([ 48 | http.get('/users').then(onSuccess), 49 | http.get('/users').then(onSuccess), 50 | ]); 51 | t.is(onSuccess.callCount, 7); 52 | t.is(adapterCb.callCount, 2); 53 | 54 | }); 55 | 56 | test('throttle adapter shouldn`t do anything when a non-get request invoked', async t => { 57 | 58 | const adapterCb = spy(); 59 | const mockedAdapter = genMockAdapter(adapterCb); 60 | const http = axios.create({ 61 | adapter: throttleAdapterEnhancer(mockedAdapter), 62 | }); 63 | 64 | const onSuccess = spy(); 65 | await Promise.all([ 66 | http.post('/users').then(onSuccess), 67 | http.post('/users').then(onSuccess), 68 | ]); 69 | t.is(onSuccess.callCount, 2); 70 | t.is(adapterCb.callCount, 2); 71 | 72 | }); 73 | 74 | test('cache will be removed when request error', async t => { 75 | 76 | const adapterCb = spy(); 77 | const mockedAdapter = genMockAdapter(adapterCb); 78 | const http = axios.create({ 79 | adapter: throttleAdapterEnhancer(mockedAdapter), 80 | }); 81 | 82 | const onSuccess = spy(); 83 | const onError = spy(); 84 | await Promise.all([ 85 | http.get('/users', { error: true } as any).then(onSuccess, onError), 86 | http.get('/users').then(onSuccess, onError), 87 | ]); 88 | t.is(onSuccess.callCount, 0); 89 | t.is(onError.callCount, 2); 90 | t.is(adapterCb.callCount, 1); 91 | 92 | await Promise.all([ 93 | http.get('/users').then(onSuccess, onError), 94 | http.get('/users').then(onSuccess, onError), 95 | ]); 96 | t.is(onSuccess.callCount, 2); 97 | t.is(adapterCb.callCount, 2); 98 | 99 | }); 100 | 101 | test('use a custom cache for throttle enhancer', async t => { 102 | 103 | const adapterCb = spy(); 104 | const mockedAdapter = genMockAdapter(adapterCb); 105 | const cache = new LRUCache({ max: 100 }); 106 | const http = axios.create({ 107 | adapter: throttleAdapterEnhancer(mockedAdapter, { cache }), 108 | }); 109 | 110 | const onSuccess = spy(); 111 | await Promise.all([ 112 | http.get('/users').then(onSuccess), 113 | http.get('/users').then(onSuccess), 114 | ]); 115 | t.is(onSuccess.callCount, 2); 116 | t.is(adapterCb.callCount, 1); 117 | 118 | cache.delete('/users'); 119 | await Promise.all([ 120 | http.get('/users').then(onSuccess), 121 | http.get('/users').then(onSuccess), 122 | ]); 123 | t.is(onSuccess.callCount, 4); 124 | t.is(adapterCb.callCount, 2); 125 | }); 126 | -------------------------------------------------------------------------------- /src/cacheAdapterEnhancer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Kuitos 3 | * @homepage https://github.com/kuitos/ 4 | * @since 2017-10-12 5 | */ 6 | 7 | import { AxiosAdapter, AxiosPromise } from 'axios'; 8 | import LRUCache from 'lru-cache'; 9 | import buildSortedURL from './utils/buildSortedURL'; 10 | import isCacheLike, { ICacheLike } from './utils/isCacheLike'; 11 | 12 | declare module 'axios' { 13 | interface AxiosRequestConfig { 14 | forceUpdate?: boolean; 15 | cache?: boolean | ICacheLike; 16 | } 17 | } 18 | 19 | const FIVE_MINUTES = 1000 * 60 * 5; 20 | const CAPACITY = 100; 21 | 22 | export type Options = { 23 | enabledByDefault?: boolean, 24 | cacheFlag?: string, 25 | defaultCache?: ICacheLike, 26 | }; 27 | 28 | export default function cacheAdapterEnhancer(adapter: AxiosAdapter, options: Options = {}): AxiosAdapter { 29 | 30 | const { 31 | enabledByDefault = true, 32 | cacheFlag = 'cache', 33 | defaultCache = new LRUCache({ ttl: FIVE_MINUTES, max: CAPACITY }), 34 | } = options; 35 | 36 | return config => { 37 | 38 | const { url, method, params, paramsSerializer, forceUpdate } = config; 39 | const useCache = ((config as any)[cacheFlag] !== void 0 && (config as any)[cacheFlag] !== null) 40 | ? (config as any)[cacheFlag] 41 | : enabledByDefault; 42 | 43 | if (method === 'get' && useCache) { 44 | 45 | // if had provided a specified cache, then use it instead 46 | const cache: ICacheLike = isCacheLike(useCache) ? useCache : defaultCache; 47 | 48 | // build the index according to the url and params 49 | const index = buildSortedURL(url, params, paramsSerializer); 50 | 51 | let responsePromise = cache.get(index); 52 | 53 | if (!responsePromise || forceUpdate) { 54 | 55 | responsePromise = (async () => { 56 | 57 | try { 58 | return await adapter(config); 59 | } catch (reason) { 60 | 'delete' in cache ? cache.delete(index) : cache.del(index); 61 | throw reason; 62 | } 63 | 64 | })(); 65 | 66 | // put the promise for the non-transformed response into cache as a placeholder 67 | cache.set(index, responsePromise); 68 | 69 | return responsePromise; 70 | } 71 | 72 | /* istanbul ignore next */ 73 | if (process.env.LOGGER_LEVEL === 'info') { 74 | // eslint-disable-next-line no-console 75 | console.info(`[axios-extensions] request cached by cache adapter --> url: ${index}`); 76 | } 77 | 78 | return responsePromise; 79 | } 80 | 81 | return adapter(config); 82 | }; 83 | } 84 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Kuitos 3 | * @homepage https://github.com/kuitos/ 4 | * @since 2017-09-28 5 | */ 6 | 7 | import Cache from 'lru-cache'; 8 | import cacheAdapterEnhancer from './cacheAdapterEnhancer'; 9 | import retryAdapterEnhancer from './retryAdapterEnhancer'; 10 | import throttleAdapterEnhancer from './throttleAdapterEnhancer'; 11 | import { ICacheLike } from './utils/isCacheLike'; 12 | 13 | export { 14 | Cache, 15 | ICacheLike, 16 | cacheAdapterEnhancer, 17 | throttleAdapterEnhancer, 18 | retryAdapterEnhancer, 19 | }; 20 | -------------------------------------------------------------------------------- /src/retryAdapterEnhancer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Kuitos 3 | * @since 2020-02-18 4 | */ 5 | 6 | import { AxiosAdapter, AxiosResponse } from 'axios'; 7 | 8 | declare module 'axios' { 9 | interface AxiosRequestConfig { 10 | retryTimes?: number; 11 | } 12 | } 13 | 14 | export type Options = { 15 | times?: number; 16 | }; 17 | 18 | export default function retryAdapterEnhancer(adapter: AxiosAdapter, options: Options = {}): AxiosAdapter { 19 | 20 | const { times = 2 } = options; 21 | 22 | return async config => { 23 | 24 | const { retryTimes = times } = config; 25 | 26 | let timeUp = false; 27 | let count = 0; 28 | const request = async (): Promise => { 29 | try { 30 | return await adapter(config); 31 | } catch (e) { 32 | timeUp = retryTimes === count; 33 | if (timeUp) { 34 | throw e; 35 | } 36 | 37 | count++; 38 | 39 | /* istanbul ignore next */ 40 | if (process.env.LOGGER_LEVEL === 'info') { 41 | console.info(`[axios-extensions] request start retrying --> url: ${config.url} , time: ${count}`); 42 | } 43 | 44 | return request(); 45 | } 46 | }; 47 | 48 | return request(); 49 | }; 50 | } 51 | -------------------------------------------------------------------------------- /src/throttleAdapterEnhancer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Kuitos 3 | * @homepage https://github.com/kuitos/ 4 | * @since 2017-10-11 5 | */ 6 | 7 | import { AxiosAdapter, AxiosPromise, InternalAxiosRequestConfig } from 'axios'; 8 | import LRUCache from 'lru-cache'; 9 | import buildSortedURL from './utils/buildSortedURL'; 10 | import { ICacheLike } from './utils/isCacheLike'; 11 | 12 | export type RecordedCache = { 13 | timestamp: number; 14 | value?: AxiosPromise; 15 | }; 16 | 17 | export type Options = { 18 | threshold?: number, 19 | cache?: ICacheLike, 20 | }; 21 | 22 | export default function throttleAdapterEnhancer(adapter: AxiosAdapter, options: Options = {}): AxiosAdapter { 23 | 24 | const { threshold = 1000, cache = new LRUCache({ max: 10 }) } = options; 25 | 26 | const recordCacheWithRequest = (index: string, config: InternalAxiosRequestConfig) => { 27 | 28 | const responsePromise = (async () => { 29 | 30 | try { 31 | const response = await adapter(config); 32 | 33 | cache.set(index, { 34 | timestamp: Date.now(), 35 | value: Promise.resolve(response), 36 | }); 37 | 38 | return response; 39 | } catch (reason) { 40 | 'delete' in cache ? cache.delete(index) : cache.del(index); 41 | throw reason; 42 | } 43 | 44 | })(); 45 | 46 | cache.set(index, { 47 | timestamp: Date.now(), 48 | value: responsePromise, 49 | }); 50 | 51 | return responsePromise; 52 | }; 53 | 54 | return config => { 55 | 56 | const { url, method, params, paramsSerializer } = config; 57 | const index = buildSortedURL(url, params, paramsSerializer); 58 | 59 | const now = Date.now(); 60 | const cachedRecord = cache.get(index) || { timestamp: now }; 61 | 62 | if (method === 'get') { 63 | 64 | if (now - cachedRecord.timestamp <= threshold) { 65 | 66 | const responsePromise = cachedRecord.value; 67 | if (responsePromise) { 68 | 69 | /* istanbul ignore next */ 70 | if (process.env.LOGGER_LEVEL === 'info') { 71 | // eslint-disable-next-line no-console 72 | console.info(`[axios-extensions] request cached by throttle adapter --> url: ${index}`); 73 | } 74 | 75 | return responsePromise; 76 | } 77 | } 78 | 79 | return recordCacheWithRequest(index, config); 80 | } 81 | 82 | return adapter(config); 83 | }; 84 | } 85 | -------------------------------------------------------------------------------- /src/utils/__tests__/test-buildSortedURL.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Kuitos 3 | * @homepage https://github.com/kuitos/ 4 | * @since 2017-10-17 5 | */ 6 | 7 | import test from 'ava'; 8 | import buildSortedURL from '../buildSortedURL'; 9 | 10 | test('build a simple url without params', t => { 11 | 12 | const url = '//cross-domain.test/users'; 13 | const params = {}; 14 | 15 | const builtUrl = buildSortedURL(url, params); 16 | t.is(builtUrl, `${url}`); 17 | }); 18 | 19 | test('build a simple url with params', t => { 20 | 21 | const url = '//cross-domain.test/users'; 22 | const params = { name: 'kuitos', age: 18 }; 23 | 24 | const builtUrl = buildSortedURL(url, params); 25 | t.is(builtUrl, `${url}?age=18&name=kuitos`); 26 | }); 27 | 28 | test('build a url which already had a query string with params', t => { 29 | 30 | const url = '//cross-domain.test/users?title=genius'; 31 | const params = { name: 'kuitos', age: 18 }; 32 | 33 | const builtUrl = buildSortedURL(url, params); 34 | t.is(builtUrl, '//cross-domain.test/users?age=18&name=kuitos&title=genius'); 35 | }); 36 | -------------------------------------------------------------------------------- /src/utils/__tests__/test-isCacheLike.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by Kuitos 3 | * @homepage https://github.com/kuitos/ 4 | * @since 2018/3/19 下午11:22 5 | */ 6 | 7 | import test from 'ava'; 8 | import isCacheLike from '../isCacheLike'; 9 | 10 | test('a object with specified method will be regard as cache', t => { 11 | 12 | let cache = {}; 13 | t.is(isCacheLike(cache), false); 14 | 15 | cache = { 16 | 17 | // tslint:disable-next-line 18 | get() { 19 | }, 20 | // tslint:disable-next-line 21 | set() { 22 | }, 23 | // tslint:disable-next-line 24 | del() { 25 | }, 26 | }; 27 | t.is(isCacheLike(cache), true); 28 | }); 29 | -------------------------------------------------------------------------------- /src/utils/buildSortedURL.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Kuitos 3 | * @homepage https://github.com/kuitos/ 4 | * @since 2017-10-12 5 | */ 6 | 7 | import axios from 'axios'; 8 | 9 | export default function buildSortedURL(...args: any[]) { 10 | const [url, params, paramsSerializer] = args; 11 | const builtURL = axios.getUri({ url, params, paramsSerializer }); 12 | 13 | const [urlPath, queryString] = builtURL.split('?'); 14 | 15 | if (queryString) { 16 | const paramsPair = queryString.split('&'); 17 | return `${urlPath}?${paramsPair.sort().join('&')}`; 18 | } 19 | 20 | return builtURL; 21 | } 22 | -------------------------------------------------------------------------------- /src/utils/isCacheLike.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Kuitos 3 | * @homepage https://github.com/kuitos/ 4 | * @since 2018-03-19 5 | */ 6 | export type ICacheLike = { 7 | get(key: string): T | undefined; 8 | set(key: string, value: T): void; 9 | } & ({ del(key: string): void } | { delete(key: string): void }); 10 | 11 | export default function isCacheLike(cache: any): cache is ICacheLike { 12 | return typeof cache.get === 'function' && typeof cache.set === 'function' && (typeof cache.delete === 'function' || typeof cache.del === 'function'); 13 | } 14 | -------------------------------------------------------------------------------- /tsconfig.esm.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig", 3 | "compilerOptions": { 4 | "module": "es2015", 5 | "outDir": "esm", 6 | "moduleResolution": "node" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "es5", 5 | /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'. */ 6 | "module": "commonjs", 7 | /* Specify module code generation: 'none', commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 8 | "lib": [ 9 | "es2015" 10 | ], 11 | /* Specify library files to be included in the compilation: */ 12 | // "allowJs": true, /* Allow javascript files to be compiled. */ 13 | // "checkJs": true, /* Report errors in .js files. */ 14 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 15 | "declaration": true, 16 | /* Generates corresponding '.d.ts' file. */ 17 | "sourceMap": true, 18 | /* Generates corresponding '.map' file. */ 19 | // "outFile": "./", /* Concatenate and emit output to single file. */ 20 | "outDir": "lib", 21 | /* Redirect output structure to the directory. */ 22 | "rootDir": "src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 23 | // "removeComments": true, /* Do not emit comments to output. */ 24 | // "noEmit": true, /* Do not emit outputs. */ 25 | "importHelpers": true, /* Import emit helpers from 'tslib'. */ 26 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 27 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 28 | 29 | /* Strict Type-Checking Options */ 30 | "strict": true, 31 | /* Enable all strict type-checking options. */ 32 | "noImplicitAny": true, 33 | /* Raise error on expressions and declarations with an implied 'any' type. */ 34 | "strictNullChecks": true, 35 | /* Enable strict null checks. */ 36 | "noImplicitThis": true, 37 | /* Raise error on 'this' expressions with an implied 'any' type. */ 38 | "alwaysStrict": true, 39 | /* Parse in strict mode and emit "use strict" for each source file. */ 40 | 41 | /* Additional Checks */ 42 | "noUnusedLocals": false, 43 | /* Report errors on unused locals. */ 44 | "noUnusedParameters": true, 45 | /* Report errors on unused parameters. */ 46 | "noImplicitReturns": true, 47 | /* Report error when not all code paths in function return a value. */ 48 | "noFallthroughCasesInSwitch": true, 49 | /* Report errors for fallthrough cases in switch statement. */ 50 | /* Module Resolution Options */ 51 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 52 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 53 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 54 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 55 | // "typeRoots": [], /* List of folders to include type definitions from. */ 56 | // "types": [], /* Type declaration files to be included in compilation. */ 57 | // "allowSyntheticDefaultImports": true /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 58 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 59 | 60 | /* Source Map Options */ 61 | // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 62 | // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */ 63 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 64 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 65 | 66 | /* Experimental Options */ 67 | "esModuleInterop": true, 68 | "allowSyntheticDefaultImports": true, 69 | "experimentalDecorators": true 70 | /* Enables experimental support for ES7 framework. */ 71 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for framework. */ 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "error", 3 | "extends": [ 4 | "tslint:recommended", 5 | "tslint-eslint-rules" 6 | ], 7 | "jsRules": {}, 8 | "rules": { 9 | "indent": [ 10 | "tabs" 11 | ], 12 | "quotemark": [ 13 | true, 14 | "single" 15 | ], 16 | "no-console": false, 17 | "arrow-parens": [ 18 | "ban-single-arg-parens" 19 | ], 20 | "object-literal-sort-keys": false, 21 | "object-curly-spacing": [ 22 | true, 23 | "always" 24 | ], 25 | "member-access": false, 26 | "interface-name": false, 27 | "member-ordering": [ 28 | true, 29 | { 30 | "order": [ 31 | "private-static-field", 32 | "public-static-field", 33 | "private-instance-field", 34 | "public-instance-field", 35 | "private-constructor", 36 | "public-constructor", 37 | "private-instance-method", 38 | "protected-instance-method", 39 | "public-static-method", 40 | "public-instance-method" 41 | ] 42 | } 43 | ], 44 | "max-classes-per-file": [true, 5, "exclude-class-expressions"], 45 | "no-unused-expression": true, 46 | "max-line-length": [true, 140], 47 | "interface-over-type-literal": false 48 | } 49 | } 50 | --------------------------------------------------------------------------------