├── .editorconfig ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── .vscode ├── launch.json └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── bin └── nettime.cjs ├── lib ├── nettime.d.ts ├── nettime.js ├── printer.js └── timings.js ├── package.json ├── pnpm-lock.yaml ├── rollup.config.js └── tests ├── cert.pem ├── cjs.cjs ├── key.pem ├── nettime.js ├── printer.js ├── timings.js └── types.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Test or Release 2 | 3 | on: 4 | - push 5 | - pull_request 6 | 7 | jobs: 8 | tets-or-release: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout Sources 12 | uses: actions/checkout@v2 13 | - name: Install Node 14 | uses: actions/setup-node@v2 15 | with: 16 | node-version: 'lts/*' 17 | registry-url: 'https://registry.npmjs.org' 18 | - name: Install PNPM 19 | uses: pnpm/action-setup@v2 20 | with: 21 | version: '>=6' 22 | run_install: | 23 | - args: [--frozen-lockfile, --no-verify-store-integrity] 24 | - name: Test 25 | run: npm test 26 | - name: Coverage 27 | uses: codecov/codecov-action@v2 28 | - name: Publish 29 | uses: cycjimmy/semantic-release-action@v2 30 | with: 31 | semantic_version: 18 32 | branches: master 33 | env: 34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 35 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | coverage 2 | lib/*.cjs 3 | node_modules 4 | pnpm-lock.yaml 5 | test.out 6 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "node", 6 | "request": "launch", 7 | "name": "bin", 8 | "program": "${workspaceRoot}/bin/nettime.cjs", 9 | "args": [ 10 | "-i", 11 | "https://www.google.com" 12 | ] 13 | }, 14 | { 15 | "type": "node", 16 | "request": "launch", 17 | "name": "test", 18 | "program": "${workspaceRoot}/tests/nettime" 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "**/coverage": true, 5 | "**/node_modules": true, 6 | "**/pnpm-lock.yaml": true 7 | }, 8 | "search.exclude": { 9 | "**/node_modules": true, 10 | "**/lib/*.cjs": true 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # [5.0.0](https://github.com/prantlf/nettime/compare/v4.0.0...v5.0.0) (2022-12-20) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * Replace commander with simple command-line parsing ([2f140c6](https://github.com/prantlf/nettime/commit/2f140c64fecb0d3289e76af669d8133af45b281a)) 7 | 8 | 9 | ### Features 10 | 11 | * Provide native ESM export ([6aa74c7](https://github.com/prantlf/nettime/commit/6aa74c754bdee75ee8566fc5a783330e85d77e7e)) 12 | 13 | 14 | ### BREAKING CHANGES 15 | 16 | * Although the same command-line syntax is supported, replacing the parser might cause a not integhded breaking change. 17 | * Node.js 14.8 is required. Although this package should work with Node.js 12 or even Node.js 10 still well, the declaration of exports in package.json is recognised reliably by Node.js 14.8 or newer. 18 | 19 | # [4.0.0](https://github.com/prantlf/nettime/compare/v3.0.1...v4.0.0) (2021-12-12) 20 | 21 | 22 | ### Features 23 | 24 | * Add TypeScript types ([6f48b4a](https://github.com/prantlf/nettime/commit/6f48b4abfe6356f4413544748b3713ca4ede2e07)) 25 | * Allow following redirects ([0736037](https://github.com/prantlf/nettime/commit/0736037e0f79c76270a0a1b48786b53da5ecd060)) 26 | 27 | 28 | ### chore 29 | 30 | * Upgrade dependencies ([5dbbff7](https://github.com/prantlf/nettime/commit/5dbbff78cd82391775b23cd8b4751696c891313f)) 31 | 32 | 33 | ### BREAKING CHANGES 34 | 35 | * Node.js has to be upgraded to 12 or newer version. 36 | 37 | 38 | 39 | ## [3.0.1](https://github.com/prantlf/nettime/compare/v3.0.0...v3.0.1) (2021-12-12) 40 | 41 | 42 | ### Bug Fixes 43 | 44 | * Specifying timeout made the connection fail immediately ([cad761e](https://github.com/prantlf/nettime/commit/cad761e2f5181b0f199cc923ccbe7d48eeade279)) 45 | 46 | 47 | 48 | # [3.0.0](https://github.com/prantlf/nettime/compare/v2.1.4...v3.0.0) (2019-10-19) 49 | 50 | 51 | ### Features 52 | 53 | * Add formatting the console output as a raw JSON (format "raw") ([f2f88f8](https://github.com/prantlf/nettime/commit/f2f88f8303ea87c1fbc97577127a4a0c7372cf71)) 54 | * Allow making multiple requests and returning their average timings ([112d581](https://github.com/prantlf/nettime/commit/112d581067fd2ebc5dab8c323be821831393ffb6)) 55 | 56 | 57 | ### BREAKING CHANGES 58 | 59 | * The source code was refactored to depend on some features available first in Node.js 8. Asynchronous async/await keywords, for example. 60 | 61 | 62 | 63 | ## [2.1.4](https://github.com/prantlf/nettime/compare/v2.1.3...v2.1.4) (2019-10-18) 64 | 65 | 66 | ### Bug Fixes 67 | 68 | * Fix typo in a keyword ([671fb7b](https://github.com/prantlf/nettime/commit/671fb7bf24d3386138e13406c196fa3f9b3048f8)) 69 | 70 | 71 | 72 | ## [2.1.3](https://github.com/prantlf/nettime/compare/v2.1.2...v2.1.3) (2019-10-18) 73 | 74 | 75 | ### Bug Fixes 76 | 77 | * Upgrade package dependencies ([c608e0a](https://github.com/prantlf/nettime/commit/c608e0add1d7bf2f398f29324b58a787f3f735fe)) 78 | * Use a global timeout handler a workaround for the idle socket setTimeout in Node.js 10+ ([1a4481f](https://github.com/prantlf/nettime/commit/1a4481ff15b41e243185bb7c0ed7f6b7cb91698f)) 79 | 80 | 81 | 82 | ## [2.1.2](https://github.com/prantlf/nettime/compare/v2.1.1...v2.1.2) (2019-10-18) 83 | 84 | 85 | ### Bug Fixes 86 | 87 | * Fix crash on Node.js 10+ caused by consuming both readable and data events ([7c86d3b](https://github.com/prantlf/nettime/commit/7c86d3bcead098aa4a660ab0219b904d60a780d0)) 88 | * Upgrade npm dependencies ([b3132b3](https://github.com/prantlf/nettime/commit/b3132b39fe0d4b4501a76031e0339c60a9dbaa4a)) 89 | 90 | 91 | 92 | ## [2.1.1](https://github.com/prantlf/nettime/compare/v2.1.0...v2.1.1) (2019-06-08) 93 | 94 | 95 | ### Bug Fixes 96 | 97 | * Upgrade module dependencies ([d54d700](https://github.com/prantlf/nettime/commit/d54d700c13c67a1bbe51dec8429aefd55c0525c4)) 98 | 99 | 100 | 101 | # [2.1.0](https://github.com/prantlf/nettime/compare/v2.0.1...v2.1.0) (2019-03-10) 102 | 103 | 104 | ### Bug Fixes 105 | 106 | * Upgrade package dependencies ([415fe74](https://github.com/prantlf/nettime/commit/415fe74be7416e7b9549f27a60d8d13bcef27c43)) 107 | 108 | 109 | ### Features 110 | 111 | * Add support for specifying connection timeout ([6edb361](https://github.com/prantlf/nettime/commit/6edb361e1e8ebd992bc10569d5e4d71ee4676dce)) 112 | 113 | 114 | 115 | ## [2.0.1](https://github.com/prantlf/nettime/compare/v2.0.0...v2.0.1) (2018-05-19) 116 | 117 | 118 | ### Bug Fixes 119 | 120 | * Adapt http2 connection for Node.js 8.11.2 and Node.js 10 ([1663f64](https://github.com/prantlf/nettime/commit/1663f64fb189b951d968183aa506db31a776ed59)) 121 | 122 | 123 | 124 | # [2.0.0](https://github.com/prantlf/nettime/compare/v1.1.2...v2.0.0) (2018-04-27) 125 | 126 | 127 | ### Bug Fixes 128 | 129 | * Upgrade NPM module dependencies ([8a464e0](https://github.com/prantlf/nettime/commit/8a464e01f59d0fc0441cec0119aa75b255ae776a)) 130 | 131 | 132 | ### chore 133 | 134 | * Dropped support of Node.js 4 ([5ac3a71](https://github.com/prantlf/nettime/commit/5ac3a7109344bbb8ffd599d1dbc8500bac3e1f07)) 135 | 136 | 137 | ### BREAKING CHANGES 138 | 139 | * Dropped support of Node.js 4 140 | 141 | 142 | 143 | ## [1.1.2](https://github.com/prantlf/nettime/compare/v1.1.1...v1.1.2) (2018-03-16) 144 | 145 | 146 | ### Bug Fixes 147 | 148 | * Upgrade package dependencies ([02a440c](https://github.com/prantlf/nettime/commit/02a440c5dac16ffda5746c2434e58979467592fe)) 149 | 150 | 151 | 152 | ## [1.1.1](https://github.com/prantlf/nettime/compare/v1.1.0...v1.1.1) (2017-12-21) 153 | 154 | 155 | ### Bug Fixes 156 | 157 | * Upgrade semantic release and other dependencies ([5c0df8a](https://github.com/prantlf/nettime/commit/5c0df8ac334520bb5113029cd441f919a74752d6)) 158 | 159 | 160 | 161 | # [1.1.0](https://github.com/prantlf/nettime/compare/v1.0.0...v1.1.0) (2017-11-11) 162 | 163 | 164 | ### Bug Fixes 165 | 166 | * Add code "ERR_INSECURE_SCHEME" to the error if https is not used for a HTTP/2 request ([5995015](https://github.com/prantlf/nettime/commit/5995015e6de72538abdb6078ed93ad0fa1a5cb48)) 167 | * Fail by default, if the operation if writing to the output file failed ([0dbf72c](https://github.com/prantlf/nettime/commit/0dbf72c1fd2c3940e61c1723e1b0db85f4841832)) 168 | * Return response headers independently on returning response content ([9af088f](https://github.com/prantlf/nettime/commit/9af088f4f652b58de16ca95611ecfb962a02a05f)) 169 | 170 | 171 | ### Features 172 | 173 | * Support (secure only) HTTP/2 requests ([a1f7a2f](https://github.com/prantlf/nettime/commit/a1f7a2f77b5b385032e0655f76693edcc1b603a7)) 174 | * Support specifying HTTP version 1.0 in the request header ([3cb303c](https://github.com/prantlf/nettime/commit/3cb303c9d3507e387a39abd47967bcdf99316010)) 175 | 176 | 177 | 178 | # [1.0.0](https://github.com/prantlf/nettime/compare/v0.5.0...v1.0.0) (2017-11-06) 179 | 180 | 181 | ### Features 182 | 183 | * Make command-line options compatible with curl ([33d2917](https://github.com/prantlf/nettime/commit/33d29176beff5ad886d11ae55348cce9438b8cc6)) 184 | 185 | 186 | ### Performance Improvements 187 | 188 | * Make command-line options compatible with curl ([527dfd5](https://github.com/prantlf/nettime/commit/527dfd5616708d87d8db23c126b8e275dd1752a8)) 189 | 190 | 191 | ### BREAKING CHANGES 192 | 193 | * Some command-line options: 194 | 195 | -e, --ignore-certificate => -k, --insecure 196 | -u, --unit => -t, --time-unit 197 | -U, --user => -u, --user 198 | 199 | 200 | 201 | # [0.5.0](https://github.com/prantlf/nettime/compare/v0.4.0...v0.5.0) (2017-11-06) 202 | 203 | 204 | ### Features 205 | 206 | * Allow sending data with the POST verb ([8a5975b](https://github.com/prantlf/nettime/commit/8a5975b105323507e30728effc9d653e473aabff)) 207 | * Allow specifying the HTTP verb on the command line ([1f055d1](https://github.com/prantlf/nettime/commit/1f055d18b2cadebc1fbd8179e1beb88e5c259421)) 208 | * Allow using the HEAD verb to show document info only ([abd440e](https://github.com/prantlf/nettime/commit/abd440e7bf0e1cedc7a6ef824238af972f168195)) 209 | * Allow writing response headers with received data to a file ([499fcc0](https://github.com/prantlf/nettime/commit/499fcc059c5554e6cea21e32b85f1bae56c7a813)) 210 | * Allow writing the received data to a file ([1fd28c2](https://github.com/prantlf/nettime/commit/1fd28c20ceee50307664874c31cd492bc9de45da)) 211 | 212 | 213 | 214 | # [0.4.0](https://github.com/prantlf/nettime/compare/v0.3.3...v0.4.0) (2017-11-06) 215 | 216 | 217 | ### Features 218 | 219 | * Allow specifying one or multiple HTTP headers ([d5c18f8](https://github.com/prantlf/nettime/commit/d5c18f847e0abda214eba30150bb00846c12c172)) 220 | * Allow specifying username and password for Basic Authentication ([7526819](https://github.com/prantlf/nettime/commit/7526819ef954b98caac72d308bc4aad8e420e85d)) 221 | 222 | 223 | 224 | ## [0.3.3](https://github.com/prantlf/nettime/compare/v0.3.2...v0.3.3) (2017-11-04) 225 | 226 | 227 | ### Bug Fixes 228 | 229 | * Do not add seconds in nanosecond precision to avoid errors ([880bf85](https://github.com/prantlf/nettime/commit/880bf85260a1556e89124364e1c07d977510cbb7)) 230 | 231 | 232 | 233 | ## [0.3.2](https://github.com/prantlf/nettime/compare/v0.3.1...v0.3.2) (2017-11-04) 234 | 235 | 236 | ### Bug Fixes 237 | 238 | * Print both HTTP status code and message ([ac1f074](https://github.com/prantlf/nettime/commit/ac1f074d9c4558f7e2f64b2782eb448fd7fa291b)) 239 | * Use either "readable" or "data" event to catch the firstByte timing ([bacfa14](https://github.com/prantlf/nettime/commit/bacfa142e42ae7015141e5f14e10c07b8e87b231)) 240 | * Use only the "data" event to catch the firstByte timing ([f5bb930](https://github.com/prantlf/nettime/commit/f5bb9307b84402b322a0cd13ee74c2809dabb267)) 241 | 242 | 243 | 244 | ## [0.3.1](https://github.com/prantlf/nettime/compare/v0.3.0...v0.3.1) (2017-10-22) 245 | 246 | 247 | ### Bug Fixes 248 | 249 | * Remove the dependency on the module "request", which is not used any more ([b06b5f4](https://github.com/prantlf/nettime/commit/b06b5f437a46b0ff4f80769f2d60f33cb28f60cd)) 250 | * Round results of divisions instead of truncating them ([2feb8dc](https://github.com/prantlf/nettime/commit/2feb8dc65be130aa53500e99877b02e029f8adb3)) 251 | 252 | 253 | 254 | # [0.3.0](https://github.com/prantlf/nettime/compare/v0.2.0...v0.3.0) (2017-10-22) 255 | 256 | 257 | ### Bug Fixes 258 | 259 | * Improve description of the timing type ([c4429fa](https://github.com/prantlf/nettime/commit/c4429fa3f827448b75870c13e2bdfb390ebc3bb0)) 260 | 261 | 262 | ### Features 263 | 264 | * Allow ignoring of TLS certificate errors ([e639341](https://github.com/prantlf/nettime/commit/e639341a23510b02ad60bf819f1041630a94970e)) 265 | 266 | 267 | 268 | # [0.2.0](https://github.com/prantlf/nettime/compare/v0.1.0...v0.2.0) (2017-10-21) 269 | 270 | 271 | ### Features 272 | 273 | * Add timing for Socket Close ([b24119a](https://github.com/prantlf/nettime/commit/b24119ab9d88e8b187088bd9787a7b9716d876dc)) 274 | 275 | 276 | 277 | # 0.1.0 (2017-10-21) 278 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017-2022 Ferdinand Prantl 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 | # nettime 2 | 3 | [![npm](https://img.shields.io/npm/v/nettime)](https://www.npmjs.com/package/nettime#top) 4 | [![codecov](https://codecov.io/gh/prantlf/nettime/branch/master/graph/badge.svg)](https://codecov.io/gh/prantlf/nettime) 5 | [![codebeat badge](https://codebeat.co/badges/9d85c898-df08-42fb-8ab9-407dc2ce2d22)](https://codebeat.co/projects/github-com-prantlf-/nettime-master) 6 | ![Dependency status](https://img.shields.io/librariesio/release/npm/nettime) 7 | 8 | Prints time duration of various stages of a HTTP/S request, like DNS lookup, TLS handshake, Time to First Byte etc. Similarly to the [time] command, which measures process timings, the `nettime` command measures HTTP/S request timings. You can find more information in [Understanding & Measuring HTTP Timings with Node.js](https://blog.risingstack.com/measuring-http-timings-node-js/). 9 | 10 | **Attention**: Command-line options changed between 0.x and 1.x versions, so that they become compatible with [curl]. If you use the `nettime` command-line tool, check the affected options: 11 | 12 | ```text 13 | -e, --ignore-certificate => -k, --insecure 14 | -u, --unit => -t, --time-unit 15 | -U, --user => -u, --user 16 | ``` 17 | 18 | The programmatic interface did not change and has remained compatible. 19 | 20 | ## Command-line usage 21 | 22 | Make sure that you have [Node.js] >= 14.8 installed. Install the `nettime` package globally and print timings of a sample web site: 23 | 24 | ```bash 25 | $ npm install -g nettime 26 | $ nettime https://www.google.com 27 | Phase Finished Duration 28 | ----------------------------------- 29 | Socket Open 0.023s 0.023s 30 | DNS Lookup 0.024s 0.001s 31 | TCP Connection 0.053s 0.029s 32 | TLS Handshake 0.133s 0.079s 33 | First Byte 0.174s 0.041s 34 | Content Transfer 0.176s 0.002s 35 | Socket Close 0.177s 0.001s 36 | ----------------------------------- 37 | Status Code: OK (200) 38 | ``` 39 | 40 | Running `nettime` without any parameters prints usage instructions: 41 | 42 | ```text 43 | Usage: nettime [options] 44 | 45 | Options: 46 | 47 | -V, --version output the version number 48 | -0, --http1.0 use HTTP 1.0 49 | --http1.1 use HTTP 1.1 (default) 50 | --http2 use HTTP 2.0 51 | -c, --connect-timeout maximum time to wait for a connection 52 | -d, --data data to be sent using the POST verb 53 | -f, --format set output format: text, json, raw 54 | -H, --header
send specific HTTP header 55 | -i, --include include response headers in the output 56 | -I, --head use HEAD verb to get document info only 57 | -k, --insecure ignore certificate errors 58 | -L, --location follow redirects 59 | -o, --output write the received data to a file 60 | -t, --time-unit set time unit: ms, s+ns 61 | -u, --user credentials for Basic Authentication 62 | -X, --request specify HTTP verb to use for the request 63 | -C, --request-count count of requests to make (default: 1) 64 | -D, --request-delay delay between two requests 65 | -A, --average-timings print an average of multiple request timings 66 | -h, --help output usage information 67 | 68 | The default output format is "text" and time unit "ms". Other options 69 | are compatible with curl. Timings are printed to the standard output. 70 | 71 | Examples: 72 | 73 | $ nettime -f json https://www.github.com 74 | $ nettime --http2 -C 3 -A https://www.google.com 75 | ``` 76 | 77 | ## Programmatic usage 78 | 79 | Make sure that you use [Node.js] >= 14.8. Install the `nettime` package locally and get time duration of waiting for the response and downloading the content of a sample web page: 80 | 81 | ```bash 82 | npm install --save nettime 83 | ``` 84 | 85 | ```js 86 | const { nettime, getDuration } = require('nettime') 87 | nettime('https://www.google.com') 88 | .then(result => { 89 | if (result.statusCode === 200) { 90 | let timings = result.timings 91 | let waiting = getDuration([0, 0], timings.firstByte) 92 | let downloading = getDuration(timings.firstByte, timings.contentTransfer) 93 | console.log('Waiting for the response:', nettime.getMilliseconds(waiting) + 'ms') 94 | console.log('Downloading the content:', nettime.getMilliseconds(downloading) + 'ms') 95 | } 96 | }) 97 | .catch(error => console.error(error)) 98 | ``` 99 | 100 | The main module exports a function which makes a HTTP/S request and returns a [Promise] to the result object. The function carries properties `nettime`, `getDuration`, `getMilliseconds` and `isRedirect`, so that the export can be consumed as an object with several static functions too: 101 | 102 | ```js 103 | const nettime = require('nettime') 104 | const { nettime, getDuration } = require('nettime') 105 | ``` 106 | 107 | The input argument is a string with a URL to make the request with, or an object with multiple properties. 108 | 109 | The input object can contain: 110 | 111 | * `url`: string with a URL to make the request with. 112 | * `credentials`: object with `username` and `password` string properties to be used for formatting of the Basic Authentication HTTP header. 113 | * `data`: string or Buffer to send to the server using the HTTP verb `POST` and the content type `application/x-www-form-urlencoded` by default. 114 | * `failOnOutputFileError`: boolean for preventing the request timing operation from failing, if writing to the output file failed. If set to `false`, the error will be printed on the standard output and the process exit code will be set to 2. It is in effect only if `outputFile` is specified. The default is `true`. 115 | * `headers`: object with header names as string keys and header values as string values. 116 | * `httpVersion`: string with the protocol version ('1.0', '1.1' or '2.0') to be sent to the server. (Node.js HTTP support is hard-coded for 1.1. There can be a difference between 1.0 and 1.1 on the server side only. Node.js supports HTTP/2 in the version 8.4.0 or newer with the --expose-http2 command-lime option and in the version 8.8.1 or newer out-of-the-box. Alternatively, you can install a "http2" module as a polyfill.) 117 | * `followRedirects`: boolean to continue making requests, if the original response contained a redirecting HTTP status code 118 | * `includeHeaders`: boolean for including property `headers` (`Object`) with response headers in the promised result object. If `outputFile` is specified, the headers are written to the beginning of the output file too. 119 | * `method`: HTTP verb to use in the HTTP request; `GET` is the default, unless `-i` or `-d` options are not set. 120 | * `outputFile`: file path to write the received data to. 121 | * `rejectUnauthorized`: boolean to refuse finishing the HTTPS request, is set to `true` (the default), if validation of the web site certificate fails; setting it to `false` makes the request ignore certificate errors. 122 | * `returnResponse`: boolean for including property `response` (`Buffer`) with the received data in the promised result object. 123 | * `requestCount`: integer for making multiple requests instead of one. 124 | * `requestDelay`: integer to introduce a delay (in milliseconds ) between each two requests. The default is 100. 125 | * `timeout`: intere to set the maximum time (in milliseconds) a single request should take before aborting it. 126 | 127 | The result object contains: 128 | 129 | * `httpVersion`: HTTP version, which the server responsed with (string). 130 | * `statusCode`: [HTTP status code] of the response (integer). 131 | * `statusMessage`: HTTP status message for the status code (string). 132 | * `timings`: object with timing properties from various stages of the request. Timing is an array with two integers - seconds and nanoseconds passed since the request has been made, as returned by [process.hrtime]. 133 | * `headers`: an optional object with the response headers, if enabled by the option `includeHeaders`. 134 | * `url`: an optional string with the requested URL, if the option `followRedirects` was set to `true`. 135 | 136 | ```js 137 | { 138 | "httpVersion": '1.1', 139 | "statusCode": 200, 140 | "statusMessage": "OK", 141 | "timings": { 142 | "socketOpen": [ 0, 13260126 ], 143 | "dnsLookup": [ 0, 13747391 ], // Optional, if hostname was specified 144 | "tcpConnection": [ 0, 152135165 ], 145 | "tlsHandshake": [ 0, 433219351 ], // Optional, if HTTPS protocol was used 146 | "firstByte": [ 1, 888887072 ], 147 | "contentTransfer": [ 1, 891221207 ], 148 | "socketClose": [ 1, 893156380 ] 149 | } 150 | } 151 | ``` 152 | 153 | If the option `requestCount` is greater than `1`, the result objects will be returned in an array of the same length as the `requestCount` value. 154 | If the option `followRedirects` us set to `true`, the result objects will be returned in an array of the length depending on the presence and count of redirecting responses. 155 | 156 | *Note*: The `time-unit` parameter affects not only the "text" output format of the command line script, but also the "json" one. If set to "ms", timing values will be printed in milliseconds. If set to "s+ns", timings will be printed as arrays in [process.hrtime]'s format. Calling the `nettime` function programmatically will always return the timings as arrays in [process.hrtime]'s format. 157 | 158 | ### Helper functions 159 | 160 | The following functions are exposed as named exports from the `nettime/lib/timings` module to help dealing with [process.hrtime]'s timing format and timings from multiple requests: 161 | 162 | * `getDuration(start, end)`: computes the difference between two timings. Expects two arrays in [process.hrtime]'s format and returns the result as an array in the same [process.hrtime]'s format. 163 | * `getMilliseconds(timing)`: converts the timing to milliseconds. Expects an array in [process.hrtime]'s format and returns the result as an integer. 164 | * `computeAverageDurations(multipleTimings)`: computes average durations from an array of event timings. The array is supposed to contain objects with the same keys as the `timings` object from the `nettime` response. The returned object will contain the same keys pointing to event durations in [process.hrtime]'s format. 165 | * `createTimingsFromDurations(timings, startTime)`: reconstructs event timings from event durations. The `timings` object is supposed to contain the same keys as the `timings` object from the `nettime` response, but pointing to event durations in [process.hrtime]'s format. The returned object will contain the same keys, but pointing to event times in [process.hrtime]'s format. The `startTime` parameter can shoft the event times. The default is no shift - `[0, 0]`. 166 | * `isRedirect(statusCode)`: checks if the HTTP status code means a redirect. Returns `true` if it is, otherwise `false`. 167 | 168 | These methods can be required separately too: 169 | 170 | ```js 171 | const { isRedirect } = require('nettime') 172 | const { 173 | getDuration, getMilliseconds, 174 | computeAverageDurations, createTimingsFromDurations 175 | } = require('nettime/lib/timings') 176 | ``` 177 | 178 | Methods `getDuration`, `getMilliseconds` and `isRedirect` are accessible also as static methods of the `nettime` function exported from the main `nettime` module. 179 | 180 | ## Contributing 181 | 182 | In lieu of a formal styleguide, take care to maintain the existing coding style. Add unit tests for any new or changed functionality. Lint and test your code using Grunt. 183 | 184 | ## License 185 | 186 | Copyright (c) 2017-2022 Ferdinand Prantl 187 | 188 | Licensed under the MIT license. 189 | 190 | [time]: https://en.wikipedia.org/wiki/Time_(Unix) 191 | [Node.js]: http://nodejs.org/ 192 | [Promise]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise 193 | [HTTP status code]: https://en.wikipedia.org/wiki/List_of_HTTP_status_codes 194 | [process.hrtime]: https://nodejs.org/api/process.html#process_process_hrtime_time 195 | [curl]: https://curl.haxx.se/ 196 | -------------------------------------------------------------------------------- /bin/nettime.cjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const { nettime, isRedirect } = require('../lib/nettime.cjs') 4 | const { 5 | computeAverageDurations, createTimingsFromDurations 6 | } = require('../lib/timings.cjs') 7 | const { printTimings } = require('../lib/printer.cjs') 8 | const readlineSync = require('readline-sync') 9 | 10 | function help() { 11 | console.log(`${require('../package.json').description} 12 | 13 | Usage: nettime [options] 14 | 15 | Options: 16 | -0|--http1.0 use HTTP 1.0 17 | --http1.1 use HTTP 1.1 (default) 18 | --http2 use HTTP 2 19 | -c|--connect-timeout maximum time to wait for a connection 20 | -d|--data data to be sent using the POST verb 21 | -f|--format set output format: text, json, raw 22 | -H|--header
send specific HTTP header 23 | -i|--include include response headers in the output 24 | -I|--head use HEAD verb to get document info only 25 | -k|--insecure ignore certificate errors 26 | -L|--location follow redirects 27 | -o|--output write the received data to a file 28 | -t|--time-unit set time unit: ms, s+ns (default: ms) 29 | -u|--user credentials for Basic Authentication 30 | -X|--request specify HTTP verb to use for the request 31 | -C|--request-count count of requests to make (default: 1) 32 | -D|--request-delay delay between two requests (default: 100ms) 33 | -A|--average-timings print an average of multiple request timings 34 | -V|--version print version number 35 | -h|--help print usage instructions 36 | 37 | The default output format is "text" and time unit "ms". Other options 38 | are compatible with curl. Timings are printed to the standard output. 39 | 40 | Examples:') 41 | $ nettime https://www.github.com 42 | $ nettime -f json https://www.gitlab.com 43 | $ nettime --http2 -C 3 -A https://www.google.com`) 44 | } 45 | 46 | function toInteger(text, name) { 47 | const number = +text 48 | if (typeof number !== 'number') { 49 | console.error(`${name} has to be a number.`) 50 | process.exit(1) 51 | } 52 | return number 53 | } 54 | 55 | function toEnum(value, values, name) { 56 | if (values.indexOf(value) < 0) { 57 | console.error(`Invalid ${name}: "${value}". Valid values are "${values.join('", "')}".`) 58 | process.exit(1) 59 | } 60 | return value 61 | } 62 | 63 | const { argv } = process 64 | const header = [] 65 | let url, timeUnit, format = 'text', user, http2, http1_0, timeout, data, 66 | head, includeHeaders, insecure, outputFile, request, followRedirects, 67 | requestCount = 1, requestDelay = 100, averageTimings 68 | 69 | for (let i = 2, l = argv.length; i < l; ++i) { 70 | const arg = argv[i] 71 | const match = /^(-|--)(no-)?([a-zA-Z0][-a-zA-Z0-2.]*)(?:=(.*))?$/.exec(arg) 72 | if (match) { 73 | const parseArg = (arg, flag) => { 74 | switch (arg) { 75 | case '0': case 'http1.0': 76 | http1_0 = flag 77 | return 78 | case 'http1.1': 79 | http1_0 = !flag 80 | return 81 | case 'http2': 82 | http2 = flag 83 | return 84 | case 'c': case 'connect-timeout': 85 | timeout = toInteger(match[4] || argv[++i], 'Timeout') 86 | return 87 | case 'd': case 'data': 88 | data = match[4] || argv[++i] 89 | return 90 | case 'f': case 'format': 91 | format = toEnum(match[4] || argv[++i], ['json', 'raw', 'text'], 'format') 92 | return 93 | case 'H': case 'header': 94 | header.push(match[4] || argv[++i]) 95 | return 96 | case 'i': case 'include': 97 | includeHeaders = flag 98 | return 99 | case 'I': case 'head': 100 | head = flag 101 | return 102 | case 'k': case 'insecure': 103 | insecure = flag 104 | return 105 | case 'L': case 'location': 106 | followRedirects = flag 107 | return 108 | case 'o': case 'output': 109 | outputFile = match[4] || argv[++i] 110 | return 111 | case 't': case 'time-unit': 112 | timeUnit = toEnum(match[4] || argv[++i], ['ms', 's+ns'], 'time unit') 113 | return 114 | case 'u': case 'user': 115 | user = match[4] || argv[++i] 116 | return 117 | case 'X': case 'request': 118 | request = match[4] || argv[++i] 119 | return 120 | case 'C': case 'request-count': 121 | requestCount = toInteger(match[4] || argv[++i], 'Request count') 122 | return 123 | case 'D': case 'request-delay': 124 | requestDelay = toInteger(match[4] || argv[++i], 'Request delay') 125 | return 126 | case 'A': case 'average-timings': 127 | averageTimings = flag 128 | return 129 | case 'V': case 'version': 130 | console.log(require('../package.json').version) 131 | process.exit(0) 132 | return 133 | case 'h': case 'help': 134 | help() 135 | process.exit(0) 136 | } 137 | console.error(`unknown option: "${arg}"`) 138 | process.exit(1) 139 | } 140 | if (match[1] === '-') { 141 | const flags = match[3].split('') 142 | for (const flag of flags) parseArg(flag, true) 143 | } else { 144 | parseArg(match[3], match[2] !== 'no-') 145 | } 146 | continue 147 | } 148 | url = arg 149 | } 150 | 151 | if (!url) { 152 | help() 153 | process.exit(0) 154 | } 155 | 156 | const formatters = { 157 | json: result => { 158 | if (timeUnit !== 's+ns') { 159 | convertToMilliseconds(result.timings) 160 | } 161 | return result 162 | }, 163 | raw: result => JSON.stringify(result), 164 | text: ({ timings, httpVersion, statusCode, statusMessage }) => 165 | printTimings(timings, timeUnit) + 166 | `\nResponse: HTTP/${httpVersion} ${statusCode} ${statusMessage}` 167 | } 168 | const formatter = formatters[format] 169 | 170 | const headers = header.reduce((result, header) => { 171 | const colon = header.indexOf(':') 172 | if (colon > 0) { 173 | const name = header 174 | .substr(0, colon) 175 | .trim() 176 | .toLowerCase() 177 | const value = header 178 | .substr(colon + 1) 179 | .trim() 180 | result[name] = value 181 | } 182 | return result 183 | }, {}) 184 | 185 | let credentials = user 186 | if (credentials) { 187 | const colon = credentials.indexOf(':') 188 | let username, password 189 | if (colon > 0) { 190 | username = credentials.substr(0, colon) 191 | password = credentials.substr(colon + 1) 192 | } else { 193 | username = credentials 194 | password = readlineSync.question('Password: ', { hideEchoBack: true }) 195 | } 196 | credentials = { username, password } 197 | } 198 | 199 | const httpVersion = http2 ? '2.0' : http1_0 ? '1.0' : '1.1' 200 | const method = request || (head ? 'HEAD' : data ? 'POST' : 'GET') 201 | const failOnOutputFileError = false 202 | const rejectUnauthorized = !insecure 203 | 204 | nettime({ 205 | httpVersion, 206 | method, 207 | url, 208 | credentials, 209 | headers, 210 | data, 211 | failOnOutputFileError, 212 | includeHeaders, 213 | outputFile, 214 | rejectUnauthorized, 215 | timeout, 216 | requestCount, 217 | requestDelay, 218 | followRedirects 219 | }) 220 | .then(results => { 221 | if (requestCount > 1) { 222 | if (averageTimings) { 223 | if (followRedirects) { 224 | results = computeRedirectableAverageTimings(results) 225 | } else { 226 | const result = computeAverageTimings(results) 227 | results = [result] 228 | } 229 | } 230 | } else if (!followRedirects) { 231 | results = [results] 232 | } 233 | return results 234 | }) 235 | .then(results => { 236 | for (const result of results) { 237 | if (followRedirects) { 238 | console.log('URL:', result.url) 239 | console.log() 240 | } 241 | console.log(formatter(result)) 242 | console.log() 243 | } 244 | }) 245 | .catch(({ message }) => { 246 | console.error(message) 247 | process.exitCode = 1 248 | }) 249 | 250 | function convertToMilliseconds (timings) { 251 | const getMilliseconds = nettime.getMilliseconds 252 | for (const timing in timings) { 253 | timings[timing] = getMilliseconds(timings[timing]) 254 | } 255 | } 256 | 257 | function computeAverageTimings (results) { 258 | checkStatusCodes() 259 | const timings = results.map(({ timings }) => timings) 260 | const averageDurations = computeAverageDurations(timings) 261 | return createAverageResult(results[0], averageDurations) 262 | 263 | function checkStatusCodes () { 264 | let firstStatusCode 265 | for (const { statusCode } of results) { 266 | if (firstStatusCode === undefined) { 267 | firstStatusCode = statusCode 268 | } else { 269 | if (firstStatusCode !== statusCode) { 270 | throw new Error(`Status code of the first request was ${firstStatusCode}, but ${statusCode} was received later.`) 271 | } 272 | } 273 | } 274 | } 275 | 276 | function createAverageResult (firstResult, averageDurations) { 277 | const { httpVersion, statusCode, statusMessage } = firstResult 278 | const timings = createTimingsFromDurations(averageDurations) 279 | return { timings, httpVersion, statusCode, statusMessage } 280 | } 281 | } 282 | 283 | function computeRedirectableAverageTimings (results) { 284 | checkStatusCodes() 285 | const resultsByURL = collectResults() 286 | const durationsByURL = collectAverageDurations() 287 | return createAverageResult() 288 | 289 | function checkStatusCodes () { 290 | let firstStatusCode 291 | for (const { statusCode } of results) { 292 | if (isRedirect(statusCode)) continue 293 | if (firstStatusCode === undefined) { 294 | firstStatusCode = statusCode 295 | } else { 296 | if (firstStatusCode !== statusCode) { 297 | throw new Error(`Status code of the first request was ${firstStatusCode}, but ${statusCode} was received later.`) 298 | } 299 | } 300 | } 301 | } 302 | 303 | function collectResults () { 304 | const resultsByURL = {} 305 | for (const result of results) { 306 | const { url } = result 307 | const results = resultsByURL[url] || (resultsByURL[url] = []) 308 | results.push(result) 309 | } 310 | return resultsByURL 311 | } 312 | 313 | function collectAverageDurations () { 314 | const durationsByURL = {} 315 | for (const url in resultsByURL) { 316 | const timings = resultsByURL[url].map(({ timings }) => timings) 317 | durationsByURL[url] = computeAverageDurations(timings) 318 | } 319 | return durationsByURL 320 | } 321 | 322 | function createAverageResult () { 323 | const results = [] 324 | for (const url in resultsByURL) { 325 | const result = extractResult(resultsByURL[url][0]) 326 | const timings = createTimingsFromDurations(durationsByURL[url]) 327 | results.push({ ...result, timings }) 328 | } 329 | return results 330 | 331 | function extractResult ({ url, httpVersion, statusCode, statusMessage }) { 332 | return { url, httpVersion, statusCode, statusMessage } 333 | } 334 | } 335 | } 336 | -------------------------------------------------------------------------------- /lib/nettime.d.ts: -------------------------------------------------------------------------------- 1 | declare interface NettimeOptions { 2 | url: string 3 | credentials?: NettimeRequestCredentials 4 | method?: string 5 | headers?: Record 6 | timeout?: number 7 | followRedirects?: boolean 8 | rejectUnauthorized?: boolean 9 | requestCount?: number 10 | requestDelay?: number 11 | outputFile?: string 12 | returnResponse?: boolean 13 | includeHeaders?: boolean 14 | appendToOutput?: unknown 15 | failOnOutputFileError?: boolean 16 | httpVersion?: string 17 | data?: unknown 18 | } 19 | 20 | declare interface NettimeRequestCredentials { 21 | username: string 22 | password: string 23 | } 24 | 25 | declare interface NettimeResponse { 26 | url?: string 27 | timings: NettimeTimings 28 | httpVersion: string 29 | statusCode: number 30 | statusMessage: string 31 | headers?: Record 32 | response?: Buffer 33 | } 34 | 35 | declare interface NettimeTimings { 36 | socketOpen: number[] 37 | dnsLookup: number[] 38 | tcpConnection: number[] 39 | tlsHandshake: number[] 40 | firstByte: number[] 41 | contentTransfer: number[] 42 | socketClose: number[] 43 | } 44 | 45 | export default function nettime(options: string | NettimeOptions): Promise 46 | export function nettime(options: string | NettimeOptions): Promise 47 | export function getDuration (start: number, end: number): number 48 | export function getMilliseconds (timings: number[]): number 49 | export function isRedirect (statusCode: number): boolean 50 | -------------------------------------------------------------------------------- /lib/nettime.js: -------------------------------------------------------------------------------- 1 | import { writeFile } from 'fs' 2 | import http from 'http' 3 | import https from 'https' 4 | import http2 from 'http2' 5 | import { EOL } from 'os' 6 | import { URL } from 'url' 7 | import { getDuration, getMilliseconds } from './timings.js' 8 | 9 | export async function nettime (options) { 10 | if (typeof options === 'string') { 11 | options = { url: options } 12 | } 13 | const { requestCount, requestDelay, followRedirects } = options 14 | const results = [] 15 | if (requestCount > 1) { 16 | for (let i = 0; i < requestCount; ++i) { 17 | await makeRedirectableRequest() 18 | if (requestDelay) { 19 | await wait(requestDelay) 20 | } 21 | options.appendToOutput = true 22 | } 23 | return results 24 | } 25 | await makeRedirectableRequest() 26 | return followRedirects ? results : results[0] 27 | 28 | async function makeRedirectableRequest () { 29 | const { url: originalUrl } = options 30 | for (;;) { 31 | const { url } = options 32 | const result = await makeSingleRequest(options) 33 | if (followRedirects) { 34 | result.url = url 35 | } 36 | results.push(result) 37 | if (!(followRedirects && isRedirect(result.statusCode))) break 38 | } 39 | options.url = originalUrl 40 | } 41 | } 42 | 43 | function wait (milliseconds) { 44 | return new Promise(resolve => setTimeout(resolve, milliseconds)) 45 | } 46 | 47 | function makeSingleRequest (options) { 48 | return new Promise((resolve, reject) => { 49 | const timings = {} 50 | const { outputFile, returnResponse, includeHeaders } = options 51 | let data = (outputFile || returnResponse) && Buffer.from([]) 52 | let response 53 | 54 | function returnResult () { 55 | const result = { timings } 56 | if (response) { 57 | const { statusCode, headers } = response 58 | result.httpVersion = response.httpVersion 59 | result.statusCode = statusCode 60 | result.statusMessage = response.statusMessage 61 | if (includeHeaders) { 62 | result.headers = headers 63 | } 64 | // Prepare the next request 65 | if (isRedirect(statusCode)) { 66 | options.url = headers.location 67 | } 68 | } 69 | if (returnResponse && data) { 70 | result.response = data 71 | } 72 | resolve(result) 73 | } 74 | 75 | function writeOutputFile () { 76 | if (includeHeaders && response) { 77 | prependOutputHeader() 78 | } 79 | const flag = options.appendToOutput ? 'a' : 'w' 80 | return new Promise(resolve => 81 | writeFile(outputFile, data, { flag }, error => { 82 | if (error) { 83 | if (options.failOnOutputFileError === false) { 84 | console.error(error.message) 85 | process.exitCode = 2 86 | } else { 87 | return reject(error) 88 | } 89 | } 90 | resolve() 91 | })) 92 | 93 | function prependOutputHeader () { 94 | const prolog = [`HTTP/${response.httpVersion} ${response.statusCode} ${response.statusMessage}`] 95 | const headers = response.headers 96 | if (headers) { 97 | const allHeaders = Object 98 | .keys(headers) 99 | .map(key => `${key}: ${headers[key]}`) 100 | Array.prototype.push.apply(prolog, allHeaders) 101 | } 102 | prolog.push(EOL) 103 | data = Buffer.concat([Buffer.from(prolog.join(EOL)), data]) 104 | } 105 | } 106 | 107 | let firstByte 108 | function checkFirstByte () { 109 | if (!firstByte) { 110 | timings.firstByte = getTiming() 111 | firstByte = true 112 | } 113 | } 114 | 115 | let socketClosed 116 | function checkSocketClosed () { 117 | if (!socketClosed) { 118 | timings.socketClose = getTiming() 119 | if (outputFile && data) { 120 | writeOutputFile().then(returnResult) 121 | } else returnResult() 122 | } 123 | } 124 | 125 | function listenToSocket (socket) { 126 | timings.socketOpen = getTiming() 127 | socket 128 | .on('lookup', () => (timings.dnsLookup = getTiming())) 129 | .on('connect', () => (timings.tcpConnection = getTiming())) 130 | .on('secureConnect', () => (timings.tlsHandshake = getTiming())) 131 | .on('close', checkSocketClosed) 132 | } 133 | 134 | function listenToResponse (response) { 135 | response 136 | .on('readable', () => { 137 | checkFirstByte() 138 | const chunk = response.read() 139 | if (data && chunk !== null) { 140 | data = Buffer.concat([data, Buffer.from(chunk)]) 141 | } 142 | }) 143 | .on('end', () => (timings.contentTransfer = getTiming())) 144 | } 145 | 146 | const parameters = getParameters() 147 | const { httpVersion } = options 148 | const { protocol: scheme } = parameters 149 | let protocol 150 | if (httpVersion === '2.0') { 151 | if (scheme !== 'https:') { 152 | const error = new Error('HTTP/2 supports only the "https:" protocol.') 153 | error.code = 'ERR_INSECURE_SCHEME' 154 | throw error 155 | } 156 | protocol = http2 157 | } else { 158 | protocol = scheme === 'http:' ? http : https 159 | } 160 | 161 | const start = process.hrtime() 162 | 163 | const request = httpVersion === '2.0' ? makeHTTP2Request() : makeHTTP1Request() 164 | 165 | const timeout = options.timeout 166 | let timeoutHandler 167 | if (timeout) { 168 | request 169 | // Stopped working in Node.js 10. Added global setTimeout temporarily. 170 | /* c8 ignore next 16 */ 171 | .on('timeout', () => { 172 | if (timeoutHandler) { 173 | clearTimeout(timeoutHandler) 174 | } 175 | request.abort() 176 | const error = new Error('Connection timed out.') 177 | error.code = 'ETIMEDOUT' 178 | reject(error) 179 | }) 180 | .on('abort', () => { 181 | if (timeoutHandler) { 182 | const error = new Error('Connection timed out.') 183 | error.code = 'ETIMEDOUT' 184 | reject(error) 185 | } 186 | }) 187 | .setTimeout(timeout) 188 | } 189 | 190 | const inputData = options.data 191 | if (inputData) { 192 | request.write(inputData) 193 | } 194 | request.end() 195 | 196 | // Workaround for Node.js 10+, which does not abort the connection attempt 197 | // any more, if it takes longer than the specified timeout. The local abort 198 | // handler will convert the abortion to a timeout in this case. 199 | if (timeout) { 200 | timeoutHandler = setTimeout(() => request.abort(), timeout) 201 | } 202 | 203 | function getParameters () { 204 | const { url, data, rejectUnauthorized, credentials } = options 205 | const headers = options.headers || {} 206 | const parameters = parseURL() 207 | setSecurity() 208 | setCredentials() 209 | setContentType() 210 | return parameters 211 | 212 | function parseURL () { 213 | const { 214 | protocol, username, password, host, hostname, port, pathname, search 215 | } = new URL(url) 216 | const auth = username 217 | ? password 218 | ? `${username}.${password}` 219 | : username 220 | : undefined 221 | const path = pathname ? pathname + search : undefined 222 | const method = options.method || (data ? 'POST' : 'GET') 223 | const agent = false 224 | return { 225 | protocol, 226 | username, 227 | password, 228 | auth, 229 | host, 230 | hostname, 231 | port, 232 | pathname, 233 | search, 234 | path, 235 | headers, 236 | method, 237 | agent 238 | } 239 | } 240 | 241 | function setSecurity () { 242 | if (rejectUnauthorized !== undefined) { 243 | parameters.rejectUnauthorized = rejectUnauthorized 244 | } 245 | } 246 | 247 | function setCredentials () { 248 | if (credentials) { 249 | const token = Buffer 250 | .from(`${credentials.username}:${credentials.password}`) 251 | .toString('base64') 252 | headers.authorization = `Basic ${token}` 253 | } 254 | } 255 | 256 | function setContentType () { 257 | if (data) { 258 | if (!headers['content-type']) { 259 | headers['content-type'] = 'application/x-www-form-urlencoded' 260 | } 261 | headers['content-length'] = Buffer.byteLength(data) 262 | } 263 | } 264 | } 265 | 266 | function makeHTTP2Request () { 267 | const origin = getOrigin() 268 | const rejectUnauthorized = parameters.rejectUnauthorized 269 | const client = protocol 270 | .connect(origin, { rejectUnauthorized }) 271 | .on('socketError', reject) 272 | .on('error', reject) 273 | listenToSocket(client.socket) 274 | 275 | const headers = parameters.headers 276 | headers[':method'] = parameters.method 277 | headers[':path'] = parameters.pathname 278 | const request = client 279 | .request(headers) 280 | .on('response', headers => { 281 | const statusCode = headers[':status'] 282 | const statusMessage = http.STATUS_CODES[statusCode] 283 | response = { headers, httpVersion, statusCode, statusMessage } 284 | }) 285 | listenToResponse(request) 286 | request 287 | .on('end', () => client.close(checkSocketClosed)) 288 | .setEncoding('utf8') 289 | return request 290 | } 291 | 292 | function makeHTTP1Request () { 293 | const request = protocol 294 | .request(parameters, localResponse => { 295 | listenToResponse(response = localResponse) 296 | response.setEncoding('utf8') 297 | }) 298 | .on('socket', listenToSocket) 299 | .on('error', reject) 300 | if (httpVersion === '1.0') { 301 | enforceHTTP10() 302 | } 303 | return request 304 | 305 | function enforceHTTP10 () { 306 | const storeHeader = request._storeHeader 307 | request._storeHeader = (firstLine, headers) => { 308 | firstLine = firstLine.replace(/HTTP\/1.1\r\n$/, 'HTTP/1.0\r\n') 309 | return storeHeader.call(request, firstLine, headers) 310 | } 311 | } 312 | } 313 | 314 | function getOrigin () { 315 | const { hostname, port } = parameters 316 | let origin = `${scheme}//${hostname}` 317 | if (port) { 318 | origin += `:${port}` 319 | } 320 | return origin 321 | } 322 | 323 | function getTiming () { 324 | return getDuration(start, process.hrtime()) 325 | } 326 | }) 327 | } 328 | 329 | export function isRedirect (statusCode) { 330 | return statusCode >= 301 && statusCode <= 308 && statusCode !== 304 331 | } 332 | 333 | nettime.nettime = nettime 334 | nettime.getDuration = getDuration 335 | nettime.getMilliseconds = getMilliseconds 336 | nettime.isRedirect = isRedirect 337 | 338 | export { nettime as default, getDuration, getMilliseconds } 339 | -------------------------------------------------------------------------------- /lib/printer.js: -------------------------------------------------------------------------------- 1 | import { events, getDuration } from './timings.js' 2 | import { sprintf } from 'sprintf-js' 3 | 4 | function printMilliseconds (timings) { 5 | let lastTiming = [0, 0] 6 | const output = [ 7 | 'Phase Finished Duration', 8 | '-----------------------------------' 9 | ] 10 | for (const event in events) { 11 | const timing = timings[event] 12 | if (timing) { 13 | const timeFraction = Math.round(timing[1] / 1e6) 14 | const duration = getDuration(lastTiming, timing) 15 | const durationFraction = Math.round(duration[1] / 1e6) 16 | output.push(sprintf('%-17s %3d.%03ds %3d.%03ds', 17 | events[event], timing[0], timeFraction, duration[0], durationFraction)) 18 | lastTiming = timing 19 | } 20 | } 21 | output.push('-----------------------------------') 22 | return output 23 | } 24 | 25 | function printNanoseconds (timings) { 26 | let lastTiming = [0, 0] 27 | const output = [ 28 | 'Phase Finished Duration', 29 | '-----------------------------------------------' 30 | ] 31 | for (const event in events) { 32 | const timing = timings[event] 33 | if (timing) { 34 | const timeFraction = Math.round(timing[1] / 1000) / 1000 35 | const duration = getDuration(lastTiming, timing) 36 | const durationFraction = Math.round(duration[1] / 1000) / 1000 37 | output.push(sprintf('%-17s %3ds %7.3fms %3ds %7.3fms', 38 | events[event], timing[0], timeFraction, duration[0], durationFraction)) 39 | lastTiming = timing 40 | } 41 | } 42 | output.push('-----------------------------------------------') 43 | return output 44 | } 45 | 46 | export function printTimings (timings, timeUnit) { 47 | const print = timeUnit === 's+ns' ? printNanoseconds : printMilliseconds 48 | return print(timings).join('\n') 49 | } 50 | -------------------------------------------------------------------------------- /lib/timings.js: -------------------------------------------------------------------------------- 1 | export const events = { 2 | socketOpen: 'Socket Open', 3 | dnsLookup: 'DNS Lookup', 4 | tcpConnection: 'TCP Connection', 5 | tlsHandshake: 'TLS Handshake', 6 | firstByte: 'First Byte', 7 | contentTransfer: 'Content Transfer', 8 | socketClose: 'Socket Close' 9 | } 10 | 11 | export function getDuration (start, end) { 12 | let seconds = end[0] - start[0] 13 | let nanoseconds = end[1] - start[1] 14 | if (nanoseconds < 0) { 15 | --seconds 16 | nanoseconds += 1e9 17 | } 18 | return [seconds, nanoseconds] 19 | } 20 | 21 | export function getMilliseconds ([seconds, nanoseconds]) { 22 | return seconds * 1000 + Math.round(nanoseconds / 1000) / 1000 23 | } 24 | 25 | export function computeAverageDurations (timings) { 26 | const timingCount = timings.length 27 | const durations = createEventDurations() 28 | computeEventDurations() 29 | checkSkippedEvents() 30 | computeEventDurationAverages() 31 | return durations 32 | 33 | function createEventDurations () { 34 | const durations = {} 35 | for (const event in events) { 36 | durations[event] = [] 37 | } 38 | return durations 39 | } 40 | 41 | function computeEventDurations () { 42 | for (const timing of timings) { 43 | let lastTime = [0, 0] 44 | for (const event in events) { 45 | const time = timing[event] 46 | if (time) { 47 | const duration = getDuration(lastTime, time) 48 | durations[event].push(duration) 49 | lastTime = time 50 | } else { 51 | durations[event].push(undefined) 52 | } 53 | } 54 | } 55 | } 56 | 57 | function checkSkippedEvents () { 58 | for (const event in events) { 59 | const durationValues = durations[event] 60 | if (durationValues[0] === undefined) { 61 | for (let i = 0; i < timingCount; ++i) { 62 | if (durationValues[i] !== undefined) { 63 | throw new Error(`Unexpected event ${event} timing of the request ${i}.`) 64 | } 65 | } 66 | } else { 67 | for (let i = 0; i < timingCount; ++i) { 68 | if (durationValues[i] === undefined) { 69 | throw new Error(`Expected event ${event} timing of the request ${i}.`) 70 | } 71 | } 72 | } 73 | } 74 | } 75 | 76 | function computeEventDurationAverages () { 77 | for (const event in events) { 78 | durations[event] = durations[event].reduce((result, duration) => 79 | duration ? [result[0] + duration[0], result[1] + duration[1]] : undefined, 80 | [0, 0]) 81 | } 82 | for (const event in events) { 83 | const duration = durations[event] 84 | if (duration) { 85 | const [seconds, nanoseconds] = duration 86 | durations[event] = [seconds / timingCount, nanoseconds / timingCount] 87 | } 88 | } 89 | } 90 | } 91 | 92 | export function createTimingsFromDurations (durations, startTime) { 93 | const timings = {} 94 | let lastTiming = startTime || [0, 0] 95 | for (const event in events) { 96 | const duration = durations[event] 97 | if (duration) { 98 | const seconds = lastTiming[0] + duration[0] 99 | const nanoseconds = lastTiming[1] + duration[1] 100 | lastTiming = timings[event] = [seconds, nanoseconds] 101 | } 102 | } 103 | return timings 104 | } 105 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nettime", 3 | "description": "Prints timings of a HTTP/S request, including DNS lookup, TLS handshake etc.", 4 | "version": "5.0.0", 5 | "homepage": "https://github.com/prantlf/nettime", 6 | "author": { 7 | "name": "Ferdinand Prantl", 8 | "email": "prantlf@gmail.com", 9 | "url": "http://prantl.tk" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "https://github.com/prantlf/nettime.git" 14 | }, 15 | "bugs": { 16 | "url": "https://github.com/prantlf/nettime/issues" 17 | }, 18 | "license": "MIT", 19 | "licenses": [ 20 | { 21 | "type": "MIT", 22 | "url": "https://github.com/prantlf/nettime/blob/master/LICENSE" 23 | } 24 | ], 25 | "engines": { 26 | "node": ">=14.8" 27 | }, 28 | "type": "module", 29 | "main": "lib/nettime.cjs", 30 | "module": "lib/nettime.js", 31 | "exports": { 32 | "require": "./lib/nettime.cjs", 33 | "import": "./lib/nettime.js" 34 | }, 35 | "types": "lib/nettime.d.ts", 36 | "bin": { 37 | "nettime": "bin/nettime.cjs" 38 | }, 39 | "files": [ 40 | "bin", 41 | "lib", 42 | "CHANGELOG.md" 43 | ], 44 | "scripts": { 45 | "prepare": "rollup -c && sed -i 's/exports.default = nettime;/module.exports = nettime;/' lib/nettime.cjs && sed -i 's/exports.\\(\\w*\\) = \\w*;//' lib/nettime.cjs", 46 | "check": "teru-cjs tests/cjs.cjs && teru-esm tests/*.js", 47 | "test": "denolint && tsc --noEmit tests/types.ts && teru-cjs tests/cjs.cjs && c8 teru-esm tests/*.js" 48 | }, 49 | "release": { 50 | "plugins": [ 51 | "@semantic-release/commit-analyzer", 52 | "@semantic-release/release-notes-generator", 53 | "@semantic-release/changelog", 54 | "@semantic-release/npm", 55 | [ 56 | "@semantic-release/github", 57 | { 58 | "failComment": false 59 | } 60 | ], 61 | "@semantic-release/git" 62 | ] 63 | }, 64 | "c8": { 65 | "check-coverage": "true", 66 | "reporter": [ 67 | "lcov", 68 | "text-summary" 69 | ], 70 | "branches": 100, 71 | "functions": 100, 72 | "lines": 100, 73 | "statements": 100 74 | }, 75 | "dependencies": { 76 | "readline-sync": "1.4.10", 77 | "sprintf-js": "1.1.2" 78 | }, 79 | "devDependencies": { 80 | "@semantic-release/changelog": "6.0.2", 81 | "@semantic-release/git": "10.0.1", 82 | "@types/node": "18.11.17", 83 | "c8": "7.12.0", 84 | "denolint": "2.0.5", 85 | "nettime": "link:", 86 | "rollup": "3.7.5", 87 | "rollup-plugin-cleanup": "3.2.1", 88 | "tehanu": "1.0.1", 89 | "tehanu-repo-coco": "1.0.0", 90 | "tehanu-teru": "1.0.0", 91 | "typescript": "4.9.4" 92 | }, 93 | "keywords": [ 94 | "nettime", 95 | "time", 96 | "net", 97 | "http", 98 | "https", 99 | "measure", 100 | "timings", 101 | "request" 102 | ] 103 | } 104 | -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: 5.4 2 | 3 | specifiers: 4 | '@semantic-release/changelog': 6.0.2 5 | '@semantic-release/git': 10.0.1 6 | '@types/node': 18.11.17 7 | c8: 7.12.0 8 | denolint: 2.0.5 9 | nettime: 'link:' 10 | readline-sync: 1.4.10 11 | rollup: 3.7.5 12 | rollup-plugin-cleanup: 3.2.1 13 | sprintf-js: 1.1.2 14 | tehanu: 1.0.1 15 | tehanu-repo-coco: 1.0.0 16 | tehanu-teru: 1.0.0 17 | typescript: 4.9.4 18 | 19 | dependencies: 20 | readline-sync: 1.4.10 21 | sprintf-js: 1.1.2 22 | 23 | devDependencies: 24 | '@semantic-release/changelog': 6.0.2 25 | '@semantic-release/git': 10.0.1 26 | '@types/node': 18.11.17 27 | c8: 7.12.0 28 | denolint: 2.0.5 29 | nettime: 'link:' 30 | rollup: 3.7.5 31 | rollup-plugin-cleanup: 3.2.1_rollup@3.7.5 32 | tehanu: 1.0.1 33 | tehanu-repo-coco: 1.0.0 34 | tehanu-teru: 1.0.0_tehanu@1.0.1 35 | typescript: 4.9.4 36 | 37 | packages: 38 | 39 | /@bcoe/v8-coverage/0.2.3: 40 | resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} 41 | dev: true 42 | 43 | /@denolint/denolint-darwin-arm64/2.0.5: 44 | resolution: {integrity: sha512-eqNVp4yMdUPBO1CWRnkbnY7EfBeBjxW3YDnLq/KJkZWQSr5mV/Ls2RZQ5N56OMpHczbJJDZq4/wx9KcGMuXZpQ==} 45 | engines: {node: '>= 12'} 46 | cpu: [arm64] 47 | os: [darwin] 48 | requiresBuild: true 49 | dev: true 50 | optional: true 51 | 52 | /@denolint/denolint-darwin-x64/2.0.5: 53 | resolution: {integrity: sha512-8l7zQiFhghA4vVE62CDTzXmzzTu4hYXp9NEJXSAE2ACx6EX0jJi9q1/goIR/KMuf2xAYbDkSTouElimsZAYLVw==} 54 | engines: {node: '>= 12'} 55 | cpu: [x64] 56 | os: [darwin] 57 | requiresBuild: true 58 | dev: true 59 | optional: true 60 | 61 | /@denolint/denolint-freebsd-x64/2.0.5: 62 | resolution: {integrity: sha512-8oOOsd2SFHvwHEfAcaUzLOIH6p9++X/Om5xkNiJUvgB+lYkR71yeHsIKbwH7TuFEVmXwoZz4dnjqzQPLJ7KPRA==} 63 | engines: {node: '>= 12'} 64 | cpu: [x64] 65 | os: [freebsd] 66 | requiresBuild: true 67 | dev: true 68 | optional: true 69 | 70 | /@denolint/denolint-linux-arm-gnueabihf/2.0.5: 71 | resolution: {integrity: sha512-bBM9s30yNMhMjwOqar1752Sg1iRpmTTS5P8ddAa4gjlz72Ff99PwObIW20tAqrJ3zo9LK8Z86yZ9VIcePs6JNQ==} 72 | engines: {node: '>= 12'} 73 | cpu: [arm] 74 | os: [linux] 75 | requiresBuild: true 76 | dev: true 77 | optional: true 78 | 79 | /@denolint/denolint-linux-arm64-gnu/2.0.5: 80 | resolution: {integrity: sha512-z6bzbs2ogFs9+ydbGaXjnOK8vy+UP3MPjh5byTY3txrm9K1OY6Apdqyd2L/dG7u6cwzoowDevHjrgO+A/vQOFA==} 81 | engines: {node: '>= 12'} 82 | cpu: [arm64] 83 | os: [linux] 84 | requiresBuild: true 85 | dev: true 86 | optional: true 87 | 88 | /@denolint/denolint-linux-x64-gnu/2.0.5: 89 | resolution: {integrity: sha512-QYfL+ss0hbBXOkXuZro24+om0aPZo0DFS6OilNdExB3WZbTrfsfbkgw71cH+9btoo07fvl/+cpKmT3gIC9xA1Q==} 90 | engines: {node: '>= 12'} 91 | cpu: [x64] 92 | os: [linux] 93 | requiresBuild: true 94 | dev: true 95 | optional: true 96 | 97 | /@denolint/denolint-linux-x64-musl/2.0.5: 98 | resolution: {integrity: sha512-ZANjYnUXycyzPKdieYhSOBP+rYknBR4RULRSJdMWGfMHDhiQsNEE0GpiPdR28SQPDVJfbNjk2saqblc0BFPvrQ==} 99 | engines: {node: '>= 12'} 100 | cpu: [x64] 101 | os: [linux] 102 | requiresBuild: true 103 | dev: true 104 | optional: true 105 | 106 | /@denolint/denolint-win32-arm64-msvc/2.0.5: 107 | resolution: {integrity: sha512-A7nk9RgJRqzqyD0cX+n+dvDKEYi//bx30pLhUsNmDcGueJQiOGJSRa7Bt3KVLRJRh+NAXRiiubgDBLplTi28jw==} 108 | engines: {node: '>= 12'} 109 | cpu: [arm64] 110 | os: [win32] 111 | requiresBuild: true 112 | dev: true 113 | optional: true 114 | 115 | /@denolint/denolint-win32-ia32-msvc/2.0.5: 116 | resolution: {integrity: sha512-VHi2w7HerJP3OSaCoLuBs4+uTDN8W0Jj8c463V919kAxtnsuIcJaRstHn3uqJw9zzkDaHpjWYkLD6DwPCUgxpQ==} 117 | engines: {node: '>= 12'} 118 | cpu: [ia32] 119 | os: [win32] 120 | requiresBuild: true 121 | dev: true 122 | optional: true 123 | 124 | /@denolint/denolint-win32-x64-msvc/2.0.5: 125 | resolution: {integrity: sha512-X+1/pKTWp7gcg5bzuOBe6QfE73lO4Ro+bG28N/qYMlJMC0z+a4acG+FK43O+Z9JZGyISwz1rCkAYXTYoOHuW7A==} 126 | engines: {node: '>= 12'} 127 | cpu: [x64] 128 | os: [win32] 129 | requiresBuild: true 130 | dev: true 131 | optional: true 132 | 133 | /@istanbuljs/schema/0.1.3: 134 | resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} 135 | engines: {node: '>=8'} 136 | dev: true 137 | 138 | /@jridgewell/resolve-uri/3.1.0: 139 | resolution: {integrity: sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==} 140 | engines: {node: '>=6.0.0'} 141 | dev: true 142 | 143 | /@jridgewell/sourcemap-codec/1.4.14: 144 | resolution: {integrity: sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==} 145 | dev: true 146 | 147 | /@jridgewell/trace-mapping/0.3.17: 148 | resolution: {integrity: sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==} 149 | dependencies: 150 | '@jridgewell/resolve-uri': 3.1.0 151 | '@jridgewell/sourcemap-codec': 1.4.14 152 | dev: true 153 | 154 | /@semantic-release/changelog/6.0.2: 155 | resolution: {integrity: sha512-jHqfTkoPbDEOAgAP18mGP53IxeMwxTISN+GwTRy9uLu58UjARoZU8ScCgWGeO2WPkEsm57H8AkyY02W2ntIlIw==} 156 | engines: {node: '>=14.17'} 157 | peerDependencies: 158 | semantic-release: '>=18.0.0' 159 | dependencies: 160 | '@semantic-release/error': 3.0.0 161 | aggregate-error: 3.1.0 162 | fs-extra: 11.1.0 163 | lodash: 4.17.21 164 | dev: true 165 | 166 | /@semantic-release/error/3.0.0: 167 | resolution: {integrity: sha512-5hiM4Un+tpl4cKw3lV4UgzJj+SmfNIDCLLw0TepzQxz9ZGV5ixnqkzIVF+3tp0ZHgcMKE+VNGHJjEeyFG2dcSw==} 168 | engines: {node: '>=14.17'} 169 | dev: true 170 | 171 | /@semantic-release/git/10.0.1: 172 | resolution: {integrity: sha512-eWrx5KguUcU2wUPaO6sfvZI0wPafUKAMNC18aXY4EnNcrZL86dEmpNVnC9uMpGZkmZJ9EfCVJBQx4pV4EMGT1w==} 173 | engines: {node: '>=14.17'} 174 | peerDependencies: 175 | semantic-release: '>=18.0.0' 176 | dependencies: 177 | '@semantic-release/error': 3.0.0 178 | aggregate-error: 3.1.0 179 | debug: 4.3.3 180 | dir-glob: 3.0.1 181 | execa: 5.1.1 182 | lodash: 4.17.21 183 | micromatch: 4.0.4 184 | p-reduce: 2.1.0 185 | transitivePeerDependencies: 186 | - supports-color 187 | dev: true 188 | 189 | /@types/istanbul-lib-coverage/2.0.4: 190 | resolution: {integrity: sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==} 191 | dev: true 192 | 193 | /@types/node/18.11.17: 194 | resolution: {integrity: sha512-HJSUJmni4BeDHhfzn6nF0sVmd1SMezP7/4F0Lq+aXzmp2xm9O7WXrUtHW/CHlYVtZUbByEvWidHqRtcJXGF2Ng==} 195 | dev: true 196 | 197 | /aggregate-error/3.1.0: 198 | resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} 199 | engines: {node: '>=8'} 200 | dependencies: 201 | clean-stack: 2.2.0 202 | indent-string: 4.0.0 203 | dev: true 204 | 205 | /ansi-regex/5.0.1: 206 | resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} 207 | engines: {node: '>=8'} 208 | dev: true 209 | 210 | /ansi-styles/4.3.0: 211 | resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} 212 | engines: {node: '>=8'} 213 | dependencies: 214 | color-convert: 2.0.1 215 | dev: true 216 | 217 | /balanced-match/1.0.2: 218 | resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} 219 | dev: true 220 | 221 | /brace-expansion/1.1.11: 222 | resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} 223 | dependencies: 224 | balanced-match: 1.0.2 225 | concat-map: 0.0.1 226 | dev: true 227 | 228 | /braces/3.0.2: 229 | resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} 230 | engines: {node: '>=8'} 231 | dependencies: 232 | fill-range: 7.0.1 233 | dev: true 234 | 235 | /c8/7.12.0: 236 | resolution: {integrity: sha512-CtgQrHOkyxr5koX1wEUmN/5cfDa2ckbHRA4Gy5LAL0zaCFtVWJS5++n+w4/sr2GWGerBxgTjpKeDclk/Qk6W/A==} 237 | engines: {node: '>=10.12.0'} 238 | hasBin: true 239 | dependencies: 240 | '@bcoe/v8-coverage': 0.2.3 241 | '@istanbuljs/schema': 0.1.3 242 | find-up: 5.0.0 243 | foreground-child: 2.0.0 244 | istanbul-lib-coverage: 3.2.0 245 | istanbul-lib-report: 3.0.0 246 | istanbul-reports: 3.1.5 247 | rimraf: 3.0.2 248 | test-exclude: 6.0.0 249 | v8-to-istanbul: 9.0.1 250 | yargs: 16.2.0 251 | yargs-parser: 20.2.9 252 | dev: true 253 | 254 | /clean-stack/2.2.0: 255 | resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} 256 | engines: {node: '>=6'} 257 | dev: true 258 | 259 | /cliui/7.0.4: 260 | resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} 261 | dependencies: 262 | string-width: 4.2.3 263 | strip-ansi: 6.0.1 264 | wrap-ansi: 7.0.0 265 | dev: true 266 | 267 | /color-convert/2.0.1: 268 | resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} 269 | engines: {node: '>=7.0.0'} 270 | dependencies: 271 | color-name: 1.1.4 272 | dev: true 273 | 274 | /color-name/1.1.4: 275 | resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} 276 | dev: true 277 | 278 | /concat-map/0.0.1: 279 | resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} 280 | dev: true 281 | 282 | /convert-source-map/1.9.0: 283 | resolution: {integrity: sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==} 284 | dev: true 285 | 286 | /cross-spawn/7.0.3: 287 | resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} 288 | engines: {node: '>= 8'} 289 | dependencies: 290 | path-key: 3.1.1 291 | shebang-command: 2.0.0 292 | which: 2.0.2 293 | dev: true 294 | 295 | /debug/4.3.3: 296 | resolution: {integrity: sha512-/zxw5+vh1Tfv+4Qn7a5nsbcJKPaSvCDhojn6FEl9vupwK2VCSDtEiEtqr8DFtzYFOdz63LBkxec7DYuc2jon6Q==} 297 | engines: {node: '>=6.0'} 298 | peerDependencies: 299 | supports-color: '*' 300 | peerDependenciesMeta: 301 | supports-color: 302 | optional: true 303 | dependencies: 304 | ms: 2.1.2 305 | dev: true 306 | 307 | /denolint/2.0.5: 308 | resolution: {integrity: sha512-ym+6akCw9NQuoaCTJf00D85Al4WG+NBcGmqKeDV+c9fCReM58kPvaT1VOZZXgPYzZTkgnDL3MJsZk+oPDYydOQ==} 309 | engines: {node: '>= 12'} 310 | requiresBuild: true 311 | optionalDependencies: 312 | '@denolint/denolint-darwin-arm64': 2.0.5 313 | '@denolint/denolint-darwin-x64': 2.0.5 314 | '@denolint/denolint-freebsd-x64': 2.0.5 315 | '@denolint/denolint-linux-arm-gnueabihf': 2.0.5 316 | '@denolint/denolint-linux-arm64-gnu': 2.0.5 317 | '@denolint/denolint-linux-x64-gnu': 2.0.5 318 | '@denolint/denolint-linux-x64-musl': 2.0.5 319 | '@denolint/denolint-win32-arm64-msvc': 2.0.5 320 | '@denolint/denolint-win32-ia32-msvc': 2.0.5 321 | '@denolint/denolint-win32-x64-msvc': 2.0.5 322 | dev: true 323 | 324 | /dir-glob/3.0.1: 325 | resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} 326 | engines: {node: '>=8'} 327 | dependencies: 328 | path-type: 4.0.0 329 | dev: true 330 | 331 | /emoji-regex/8.0.0: 332 | resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} 333 | dev: true 334 | 335 | /escalade/3.1.1: 336 | resolution: {integrity: sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==} 337 | engines: {node: '>=6'} 338 | dev: true 339 | 340 | /estree-walker/0.6.1: 341 | resolution: {integrity: sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w==} 342 | dev: true 343 | 344 | /execa/5.1.1: 345 | resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==} 346 | engines: {node: '>=10'} 347 | dependencies: 348 | cross-spawn: 7.0.3 349 | get-stream: 6.0.1 350 | human-signals: 2.1.0 351 | is-stream: 2.0.1 352 | merge-stream: 2.0.0 353 | npm-run-path: 4.0.1 354 | onetime: 5.1.2 355 | signal-exit: 3.0.6 356 | strip-final-newline: 2.0.0 357 | dev: true 358 | 359 | /fill-range/7.0.1: 360 | resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} 361 | engines: {node: '>=8'} 362 | dependencies: 363 | to-regex-range: 5.0.1 364 | dev: true 365 | 366 | /find-up/5.0.0: 367 | resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} 368 | engines: {node: '>=10'} 369 | dependencies: 370 | locate-path: 6.0.0 371 | path-exists: 4.0.0 372 | dev: true 373 | 374 | /foreground-child/2.0.0: 375 | resolution: {integrity: sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==} 376 | engines: {node: '>=8.0.0'} 377 | dependencies: 378 | cross-spawn: 7.0.3 379 | signal-exit: 3.0.6 380 | dev: true 381 | 382 | /fs-extra/11.1.0: 383 | resolution: {integrity: sha512-0rcTq621PD5jM/e0a3EJoGC/1TC5ZBCERW82LQuwfGnCa1V8w7dpYH1yNu+SLb6E5dkeCBzKEyLGlFrnr+dUyw==} 384 | engines: {node: '>=14.14'} 385 | dependencies: 386 | graceful-fs: 4.2.8 387 | jsonfile: 6.1.0 388 | universalify: 2.0.0 389 | dev: true 390 | 391 | /fs.realpath/1.0.0: 392 | resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} 393 | dev: true 394 | 395 | /fsevents/2.3.2: 396 | resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} 397 | engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 398 | os: [darwin] 399 | requiresBuild: true 400 | dev: true 401 | optional: true 402 | 403 | /get-caller-file/2.0.5: 404 | resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} 405 | engines: {node: 6.* || 8.* || >= 10.*} 406 | dev: true 407 | 408 | /get-stream/6.0.1: 409 | resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} 410 | engines: {node: '>=10'} 411 | dev: true 412 | 413 | /glob/7.2.3: 414 | resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} 415 | dependencies: 416 | fs.realpath: 1.0.0 417 | inflight: 1.0.6 418 | inherits: 2.0.4 419 | minimatch: 3.1.2 420 | once: 1.4.0 421 | path-is-absolute: 1.0.1 422 | dev: true 423 | 424 | /globalyzer/0.1.0: 425 | resolution: {integrity: sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==} 426 | dev: true 427 | 428 | /globrex/0.1.2: 429 | resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} 430 | dev: true 431 | 432 | /graceful-fs/4.2.8: 433 | resolution: {integrity: sha512-qkIilPUYcNhJpd33n0GBXTB1MMPp14TxEsEs0pTrsSVucApsYzW5V+Q8Qxhik6KU3evy+qkAAowTByymK0avdg==} 434 | dev: true 435 | 436 | /has-flag/4.0.0: 437 | resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} 438 | engines: {node: '>=8'} 439 | dev: true 440 | 441 | /html-escaper/2.0.2: 442 | resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} 443 | dev: true 444 | 445 | /human-signals/2.1.0: 446 | resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} 447 | engines: {node: '>=10.17.0'} 448 | dev: true 449 | 450 | /indent-string/4.0.0: 451 | resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} 452 | engines: {node: '>=8'} 453 | dev: true 454 | 455 | /inflight/1.0.6: 456 | resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} 457 | dependencies: 458 | once: 1.4.0 459 | wrappy: 1.0.2 460 | dev: true 461 | 462 | /inherits/2.0.4: 463 | resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} 464 | dev: true 465 | 466 | /is-fullwidth-code-point/3.0.0: 467 | resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} 468 | engines: {node: '>=8'} 469 | dev: true 470 | 471 | /is-number/7.0.0: 472 | resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} 473 | engines: {node: '>=0.12.0'} 474 | dev: true 475 | 476 | /is-stream/2.0.1: 477 | resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} 478 | engines: {node: '>=8'} 479 | dev: true 480 | 481 | /isexe/2.0.0: 482 | resolution: {integrity: sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=} 483 | dev: true 484 | 485 | /istanbul-lib-coverage/3.2.0: 486 | resolution: {integrity: sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==} 487 | engines: {node: '>=8'} 488 | dev: true 489 | 490 | /istanbul-lib-report/3.0.0: 491 | resolution: {integrity: sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==} 492 | engines: {node: '>=8'} 493 | dependencies: 494 | istanbul-lib-coverage: 3.2.0 495 | make-dir: 3.1.0 496 | supports-color: 7.2.0 497 | dev: true 498 | 499 | /istanbul-reports/3.1.5: 500 | resolution: {integrity: sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w==} 501 | engines: {node: '>=8'} 502 | dependencies: 503 | html-escaper: 2.0.2 504 | istanbul-lib-report: 3.0.0 505 | dev: true 506 | 507 | /js-cleanup/1.2.0: 508 | resolution: {integrity: sha512-JeDD0yiiSt80fXzAVa/crrS0JDPQljyBG/RpOtaSbyDq03VHa9szJWMaWOYU/bcTn412uMN2MxApXq8v79cUiQ==} 509 | engines: {node: ^10.14.2 || >=12.0.0} 510 | dependencies: 511 | magic-string: 0.25.9 512 | perf-regexes: 1.0.1 513 | skip-regex: 1.0.2 514 | dev: true 515 | 516 | /jsonfile/6.1.0: 517 | resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} 518 | dependencies: 519 | universalify: 2.0.0 520 | optionalDependencies: 521 | graceful-fs: 4.2.8 522 | dev: true 523 | 524 | /locate-path/6.0.0: 525 | resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} 526 | engines: {node: '>=10'} 527 | dependencies: 528 | p-locate: 5.0.0 529 | dev: true 530 | 531 | /lodash/4.17.21: 532 | resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} 533 | dev: true 534 | 535 | /magic-string/0.25.9: 536 | resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==} 537 | dependencies: 538 | sourcemap-codec: 1.4.8 539 | dev: true 540 | 541 | /make-dir/3.1.0: 542 | resolution: {integrity: sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==} 543 | engines: {node: '>=8'} 544 | dependencies: 545 | semver: 6.3.0 546 | dev: true 547 | 548 | /merge-stream/2.0.0: 549 | resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} 550 | dev: true 551 | 552 | /micromatch/4.0.4: 553 | resolution: {integrity: sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==} 554 | engines: {node: '>=8.6'} 555 | dependencies: 556 | braces: 3.0.2 557 | picomatch: 2.3.0 558 | dev: true 559 | 560 | /mimic-fn/2.1.0: 561 | resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} 562 | engines: {node: '>=6'} 563 | dev: true 564 | 565 | /minimatch/3.1.2: 566 | resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} 567 | dependencies: 568 | brace-expansion: 1.1.11 569 | dev: true 570 | 571 | /ms/2.1.2: 572 | resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} 573 | dev: true 574 | 575 | /npm-run-path/4.0.1: 576 | resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==} 577 | engines: {node: '>=8'} 578 | dependencies: 579 | path-key: 3.1.1 580 | dev: true 581 | 582 | /once/1.4.0: 583 | resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} 584 | dependencies: 585 | wrappy: 1.0.2 586 | dev: true 587 | 588 | /onetime/5.1.2: 589 | resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} 590 | engines: {node: '>=6'} 591 | dependencies: 592 | mimic-fn: 2.1.0 593 | dev: true 594 | 595 | /p-limit/3.1.0: 596 | resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} 597 | engines: {node: '>=10'} 598 | dependencies: 599 | yocto-queue: 0.1.0 600 | dev: true 601 | 602 | /p-locate/5.0.0: 603 | resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} 604 | engines: {node: '>=10'} 605 | dependencies: 606 | p-limit: 3.1.0 607 | dev: true 608 | 609 | /p-reduce/2.1.0: 610 | resolution: {integrity: sha512-2USApvnsutq8uoxZBGbbWM0JIYLiEMJ9RlaN7fAzVNb9OZN0SHjjTTfIcb667XynS5Y1VhwDJVDa72TnPzAYWw==} 611 | engines: {node: '>=8'} 612 | dev: true 613 | 614 | /path-exists/4.0.0: 615 | resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} 616 | engines: {node: '>=8'} 617 | dev: true 618 | 619 | /path-is-absolute/1.0.1: 620 | resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} 621 | engines: {node: '>=0.10.0'} 622 | dev: true 623 | 624 | /path-key/3.1.1: 625 | resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} 626 | engines: {node: '>=8'} 627 | dev: true 628 | 629 | /path-type/4.0.0: 630 | resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} 631 | engines: {node: '>=8'} 632 | dev: true 633 | 634 | /perf-regexes/1.0.1: 635 | resolution: {integrity: sha512-L7MXxUDtqr4PUaLFCDCXBfGV/6KLIuSEccizDI7JxT+c9x1G1v04BQ4+4oag84SHaCdrBgQAIs/Cqn+flwFPng==} 636 | engines: {node: '>=6.14'} 637 | dev: true 638 | 639 | /picomatch/2.3.0: 640 | resolution: {integrity: sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==} 641 | engines: {node: '>=8.6'} 642 | dev: true 643 | 644 | /readline-sync/1.4.10: 645 | resolution: {integrity: sha512-gNva8/6UAe8QYepIQH/jQ2qn91Qj0B9sYjMBBs3QOB8F2CXcKgLxQaJRP76sWVRQt+QU+8fAkCbCvjjMFu7Ycw==} 646 | engines: {node: '>= 0.8.0'} 647 | dev: false 648 | 649 | /require-directory/2.1.1: 650 | resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} 651 | engines: {node: '>=0.10.0'} 652 | dev: true 653 | 654 | /rimraf/3.0.2: 655 | resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} 656 | hasBin: true 657 | dependencies: 658 | glob: 7.2.3 659 | dev: true 660 | 661 | /rollup-plugin-cleanup/3.2.1_rollup@3.7.5: 662 | resolution: {integrity: sha512-zuv8EhoO3TpnrU8MX8W7YxSbO4gmOR0ny06Lm3nkFfq0IVKdBUtHwhVzY1OAJyNCIAdLiyPnOrU0KnO0Fri1GQ==} 663 | engines: {node: ^10.14.2 || >=12.0.0} 664 | peerDependencies: 665 | rollup: '>=2.0' 666 | dependencies: 667 | js-cleanup: 1.2.0 668 | rollup: 3.7.5 669 | rollup-pluginutils: 2.8.2 670 | dev: true 671 | 672 | /rollup-pluginutils/2.8.2: 673 | resolution: {integrity: sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==} 674 | dependencies: 675 | estree-walker: 0.6.1 676 | dev: true 677 | 678 | /rollup/3.7.5: 679 | resolution: {integrity: sha512-z0ZbqHBtS/et2EEUKMrAl2CoSdwN7ZPzL17UMiKN9RjjqHShTlv7F9J6ZJZJNREYjBh3TvBrdfjkFDIXFNeuiQ==} 680 | engines: {node: '>=14.18.0', npm: '>=8.0.0'} 681 | hasBin: true 682 | optionalDependencies: 683 | fsevents: 2.3.2 684 | dev: true 685 | 686 | /semver/6.3.0: 687 | resolution: {integrity: sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==} 688 | hasBin: true 689 | dev: true 690 | 691 | /shebang-command/2.0.0: 692 | resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} 693 | engines: {node: '>=8'} 694 | dependencies: 695 | shebang-regex: 3.0.0 696 | dev: true 697 | 698 | /shebang-regex/3.0.0: 699 | resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} 700 | engines: {node: '>=8'} 701 | dev: true 702 | 703 | /signal-exit/3.0.6: 704 | resolution: {integrity: sha512-sDl4qMFpijcGw22U5w63KmD3cZJfBuFlVNbVMKje2keoKML7X2UzWbc4XrmEbDwg0NXJc3yv4/ox7b+JWb57kQ==} 705 | dev: true 706 | 707 | /skip-regex/1.0.2: 708 | resolution: {integrity: sha512-pEjMUbwJ5Pl/6Vn6FsamXHXItJXSRftcibixDmNCWbWhic0hzHrwkMZo0IZ7fMRH9KxcWDFSkzhccB4285PutA==} 709 | engines: {node: '>=4.2'} 710 | dev: true 711 | 712 | /sourcemap-codec/1.4.8: 713 | resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} 714 | deprecated: Please use @jridgewell/sourcemap-codec instead 715 | dev: true 716 | 717 | /sprintf-js/1.1.2: 718 | resolution: {integrity: sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==} 719 | dev: false 720 | 721 | /string-width/4.2.3: 722 | resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} 723 | engines: {node: '>=8'} 724 | dependencies: 725 | emoji-regex: 8.0.0 726 | is-fullwidth-code-point: 3.0.0 727 | strip-ansi: 6.0.1 728 | dev: true 729 | 730 | /strip-ansi/6.0.1: 731 | resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} 732 | engines: {node: '>=8'} 733 | dependencies: 734 | ansi-regex: 5.0.1 735 | dev: true 736 | 737 | /strip-final-newline/2.0.0: 738 | resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==} 739 | engines: {node: '>=6'} 740 | dev: true 741 | 742 | /supports-color/7.2.0: 743 | resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} 744 | engines: {node: '>=8'} 745 | dependencies: 746 | has-flag: 4.0.0 747 | dev: true 748 | 749 | /tehanu-repo-coco/1.0.0: 750 | resolution: {integrity: sha512-5EtlmoYStrYmj0ODjYP+jcbMwQCxYpxY9qtfbrtqfDcagHusZlhYNPz1hhh7ykxhIwy5p3Yp/+lO5ZWbhbHdIg==} 751 | engines: {node: '>=14.13'} 752 | dev: true 753 | 754 | /tehanu-teru/1.0.0_tehanu@1.0.1: 755 | resolution: {integrity: sha512-GzUu9HxSzxnj1JHkwe1FZBMoVgsDoR2f+w+fDJSKSGzw5bW0zkOeTSreS1amqVaEC0ePPJlb/lbS8Qppo3yZxA==} 756 | engines: {node: '>=14.13'} 757 | hasBin: true 758 | peerDependencies: 759 | tehanu: ^1.0.0 760 | dependencies: 761 | tehanu: 1.0.1 762 | tiny-glob: 0.2.9 763 | dev: true 764 | 765 | /tehanu/1.0.1: 766 | resolution: {integrity: sha512-Ky9M6g/G/b+JI60CRvwLhMsxso8Ki3JQjY0m+p0uFX+3qozTE5EfanAeEp/6fKfeDmJp/8uA4zCCLYYH8zLhyA==} 767 | engines: {node: '>=12'} 768 | dev: true 769 | 770 | /test-exclude/6.0.0: 771 | resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} 772 | engines: {node: '>=8'} 773 | dependencies: 774 | '@istanbuljs/schema': 0.1.3 775 | glob: 7.2.3 776 | minimatch: 3.1.2 777 | dev: true 778 | 779 | /tiny-glob/0.2.9: 780 | resolution: {integrity: sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==} 781 | dependencies: 782 | globalyzer: 0.1.0 783 | globrex: 0.1.2 784 | dev: true 785 | 786 | /to-regex-range/5.0.1: 787 | resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} 788 | engines: {node: '>=8.0'} 789 | dependencies: 790 | is-number: 7.0.0 791 | dev: true 792 | 793 | /typescript/4.9.4: 794 | resolution: {integrity: sha512-Uz+dTXYzxXXbsFpM86Wh3dKCxrQqUcVMxwU54orwlJjOpO3ao8L7j5lH+dWfTwgCwIuM9GQ2kvVotzYJMXTBZg==} 795 | engines: {node: '>=4.2.0'} 796 | hasBin: true 797 | dev: true 798 | 799 | /universalify/2.0.0: 800 | resolution: {integrity: sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==} 801 | engines: {node: '>= 10.0.0'} 802 | dev: true 803 | 804 | /v8-to-istanbul/9.0.1: 805 | resolution: {integrity: sha512-74Y4LqY74kLE6IFyIjPtkSTWzUZmj8tdHT9Ii/26dvQ6K9Dl2NbEfj0XgU2sHCtKgt5VupqhlO/5aWuqS+IY1w==} 806 | engines: {node: '>=10.12.0'} 807 | dependencies: 808 | '@jridgewell/trace-mapping': 0.3.17 809 | '@types/istanbul-lib-coverage': 2.0.4 810 | convert-source-map: 1.9.0 811 | dev: true 812 | 813 | /which/2.0.2: 814 | resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} 815 | engines: {node: '>= 8'} 816 | hasBin: true 817 | dependencies: 818 | isexe: 2.0.0 819 | dev: true 820 | 821 | /wrap-ansi/7.0.0: 822 | resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} 823 | engines: {node: '>=10'} 824 | dependencies: 825 | ansi-styles: 4.3.0 826 | string-width: 4.2.3 827 | strip-ansi: 6.0.1 828 | dev: true 829 | 830 | /wrappy/1.0.2: 831 | resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} 832 | dev: true 833 | 834 | /y18n/5.0.8: 835 | resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} 836 | engines: {node: '>=10'} 837 | dev: true 838 | 839 | /yargs-parser/20.2.9: 840 | resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} 841 | engines: {node: '>=10'} 842 | dev: true 843 | 844 | /yargs/16.2.0: 845 | resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} 846 | engines: {node: '>=10'} 847 | dependencies: 848 | cliui: 7.0.4 849 | escalade: 3.1.1 850 | get-caller-file: 2.0.5 851 | require-directory: 2.1.1 852 | string-width: 4.2.3 853 | y18n: 5.0.8 854 | yargs-parser: 20.2.9 855 | dev: true 856 | 857 | /yocto-queue/0.1.0: 858 | resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} 859 | engines: {node: '>=10'} 860 | dev: true 861 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import cleanup from 'rollup-plugin-cleanup' 2 | 3 | export default [ 4 | { 5 | input: 'lib/nettime.js', 6 | output: { 7 | file: 'lib/nettime.cjs', 8 | format: 'cjs', 9 | exports: 'named' 10 | }, 11 | external: ['fs', 'http', 'http2', 'https', 'os', 'url'], 12 | plugins: [cleanup()] 13 | }, 14 | { 15 | input: 'lib/printer.js', 16 | output: { 17 | file: 'lib/printer.cjs', 18 | format: 'cjs' 19 | }, 20 | external: ['sprintf-js'], 21 | plugins: [cleanup()] 22 | }, 23 | { 24 | input: 'lib/timings.js', 25 | output: { 26 | file: 'lib/timings.cjs', 27 | format: 'cjs' 28 | }, 29 | plugins: [cleanup()] 30 | } 31 | ] -------------------------------------------------------------------------------- /tests/cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDbDCCAlQCCQCwgip5KzSq+jANBgkqhkiG9w0BAQsFADB4MQswCQYDVQQGEwJY 3 | WDENMAsGA1UECAwEVGVzdDENMAsGA1UEBwwEVGVzdDENMAsGA1UECgwEVGVzdDEN 4 | MAsGA1UECwwEVGVzdDEQMA4GA1UEAwwHdGVzdC54eDEbMBkGCSqGSIb3DQEJARYM 5 | dGVzdEB0ZXN0Lnh4MB4XDTE3MTAyMTE4MDY1OVoXDTE4MTAyMTE4MDY1OVoweDEL 6 | MAkGA1UEBhMCWFgxDTALBgNVBAgMBFRlc3QxDTALBgNVBAcMBFRlc3QxDTALBgNV 7 | BAoMBFRlc3QxDTALBgNVBAsMBFRlc3QxEDAOBgNVBAMMB3Rlc3QueHgxGzAZBgkq 8 | hkiG9w0BCQEWDHRlc3RAdGVzdC54eDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC 9 | AQoCggEBAKMtOsBrOcDw6gfZXtYykvOEL3xXTNx1nX6QDy3mFKocWJiPrDAuNza9 10 | WzfWvUnaYSVQSvaz/Ps7lkZmkaZGeAeUsgSUR8JoI4kn4RgYl9a1aYT8sjhvmWC2 11 | /6qk/SgWlWOkHSbdF8uIIv0H78BDJM9lSrsuhttZdJHqnxC2QLlWPf4wFVZ8+KZn 12 | +JzJqOvcp3d1Up0oD++qAJl8QpCMG64Yba1DnwaCfzyyLGKzf4gxJwW/k3HmwX9P 13 | wEKMdVJxDjnRqmX39lYl8485A9UTKlE6hsDK4oRq7RAmy8uuI5vzJ5gVQoJ+LNre 14 | zSBf8nJ5/1PMs5WPVI7Q32mD/FXgSrsCAwEAATANBgkqhkiG9w0BAQsFAAOCAQEA 15 | hAGDAu90tn7vkQ3yi9cSsmqlNdf5B36q4xRcd+X/oWqQuJX1YJ+KZ+F+dbUYh/pa 16 | 29d7iqsbpMj2dyyj1rvzntStmgbUrM351QjLixcvuhXfgcs5SQqKzm9ls03SRxMm 17 | oqzqJyjLKz8s23BDHsQiiZye2l4+FQl2lYPGwa1B7jWwPT3ICYgXgtlrz4dQ4sFk 18 | It5BLe5aDRsLyDCwYKnpeaQ9GsG5yICmwJoRrG99Ihx/HOYxPHohGucEOkl/fBoO 19 | 3RPUBDY6XB2ahJjJn97E39wcGR7p+Ykc3SZgWJgAn9XlnvbvkzeKJVQpGBGAGZ3S 20 | Q6c5DP+3uqHIRt77F3snug== 21 | -----END CERTIFICATE----- 22 | -------------------------------------------------------------------------------- /tests/cjs.cjs: -------------------------------------------------------------------------------- 1 | const test = require('tehanu')(__filename) 2 | const { strictEqual } = require('assert') 3 | const nettime = require('nettime') 4 | const { 5 | events, getDuration, getMilliseconds, computeAverageDurations, createTimingsFromDurations 6 | } = require('../lib/timings.cjs') 7 | const { printTimings } = require('../lib/printer.cjs') 8 | 9 | test('test carried exported methods', () => { 10 | strictEqual(nettime.nettime, nettime) 11 | strictEqual(typeof nettime.getDuration, 'function') 12 | strictEqual(typeof nettime.getMilliseconds, 'function') 13 | strictEqual(typeof nettime.isRedirect, 'function') 14 | strictEqual(typeof events, 'object') 15 | strictEqual(typeof getDuration, 'function') 16 | strictEqual(typeof getMilliseconds, 'function') 17 | strictEqual(typeof computeAverageDurations, 'function') 18 | strictEqual(typeof createTimingsFromDurations, 'function') 19 | strictEqual(typeof printTimings, 'function') 20 | }) 21 | -------------------------------------------------------------------------------- /tests/key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpAIBAAKCAQEAoy06wGs5wPDqB9le1jKS84QvfFdM3HWdfpAPLeYUqhxYmI+s 3 | MC43Nr1bN9a9SdphJVBK9rP8+zuWRmaRpkZ4B5SyBJRHwmgjiSfhGBiX1rVphPyy 4 | OG+ZYLb/qqT9KBaVY6QdJt0Xy4gi/QfvwEMkz2VKuy6G21l0keqfELZAuVY9/jAV 5 | Vnz4pmf4nMmo69ynd3VSnSgP76oAmXxCkIwbrhhtrUOfBoJ/PLIsYrN/iDEnBb+T 6 | cebBf0/AQox1UnEOOdGqZff2ViXzjzkD1RMqUTqGwMrihGrtECbLy64jm/MnmBVC 7 | gn4s2t7NIF/ycnn/U8yzlY9UjtDfaYP8VeBKuwIDAQABAoIBAEUCyvc4IgvUl9XL 8 | +8zxRK8St5aJwWr3ny04tgK+bPzo0htrn/IciaNwABUkj5edcTp7s8iUpKiIMe0C 9 | UhUVdowWOhevso2ox2apZAxx0j2vKbphuOofYKnDm2tLLfn3WyTx8pZOKVMd91Y2 10 | NhKFExtXhnyVl9lLAv73UGCcA9Gd1+OV2pzsZ8wGWytkl02XulYsTrMAixntXMnl 11 | 9jNSqwBNgv0BZ5JcISBsmYbS20UazqNjXdDmxKWvPSnYLIy2dRv81UhP6AV4HjSs 12 | EKjrB9hM32fowDXkgD9CXWCNDgeGFM69Zc/BkaTVtanb6C2Z7ugdz3TzpkrcC06P 13 | kcJSZjkCgYEAztlyNpYb5AfFIexmKVMlXHJtBhZGWIPKIpO5LjhC0G/hnk/Rw6Mc 14 | zb5oN5KTvlit6o5kIOekVtSObjoCa5eKlHcA95wSvEZcJQRVkgVPeEeDM52GY+ER 15 | BRGYBZ+xdtrQF/ob2FkXnSQniU8Q8R4lfV7rrMolRcSieIMbT6MDifUCgYEAyfMx 16 | GWdJXVK45vJywA3p+QffCGEYGSxOffHvJ1wOTvmHZwmKFmQvYUMyuknYMQ4O8goA 17 | Hi7tKVEQOg4t7k8O6dQ8OU2vZSUmfsMY9yCOcRFBZklUGk72blP38OF+NRD5RflM 18 | zacaF1NEL7bg6MQBC2jLyYYWGS9eu8Zz5yMeI+8CgYEAjRiGb/W00Lb8IUe/6DBv 19 | K8Bh3eUT1w0OtMdPade2u7eVjwejbm+1FiLrs/yoCw8ykuzOICPiVdhnz4iCXiHg 20 | xaTnY/9ySDs2X4m1VQWKT+F3/Z1WLos3sN1vdWaZBxn7GF/i3pDnKqmezmrAg7is 21 | mfhFinfZNN4MdWf5GTl5EIECgYEAjD5eso2P3Uc9MOTd25HOEirtMBx9Z73lJIGG 22 | 24aKST3wUhXF15brcFgCOmxdvnNM3bkkK9Ha0P6Cjk6ahwxQBwJkcEcKrusFuLIz 23 | /CqXwN2C1U3HIh4D9MpLPPTbeG65LWbbd1W8QMaKa7hMqFi1gP7dxq1fW04SM8S1 24 | aKZyn/ECgYA+/P+TmCEMlholNd/+iFj9Fu/4NKNPkWDH/51gx14eXl8dS+utPPMb 25 | 59nC65Mat+QYK3iZJjOB9TEPxBKXwaCNpJJJ/B5XrQ8dvpl8/eWVneyjQlRwAmyM 26 | 3EdwgCaC1VWEKvx9O5c5obf9MAFPOgFlG6S9Xz5wqomEqEYOoyWv4w== 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /tests/nettime.js: -------------------------------------------------------------------------------- 1 | import { readFileSync, statSync, unlinkSync } from 'fs' 2 | import http from 'http' 3 | import http2 from 'http2' 4 | import https from 'https' 5 | import { dirname, join } from 'path' 6 | import { fileURLToPath } from 'url' 7 | import tehanu from 'tehanu' 8 | import { equal, fail, ok, strictEqual } from 'assert' 9 | import { nettime, isRedirect } from 'nettime' 10 | import { getDuration, getMilliseconds } from '../lib/timings.js' 11 | 12 | const test = tehanu(import.meta.url) 13 | const __dirname = dirname(fileURLToPath(import.meta.url)) 14 | 15 | const ipAddress = '127.0.0.1' 16 | const insecurePort = 8899 17 | const securePort = 9988 18 | const http2Port = 9898 19 | const servers = [] 20 | let lastRequest 21 | 22 | function createServer (protocol, port, options) { 23 | if (protocol) { 24 | return new Promise((resolve, reject) => { 25 | const creator = protocol.createSecureServer || protocol.createServer 26 | const server = options ? creator(options, serve) : protocol.createServer(serve) 27 | server 28 | .on('error', reject) 29 | .listen(port, '::', () => { 30 | servers.push(server) 31 | resolve() 32 | }) 33 | }) 34 | } 35 | } 36 | 37 | function readCertificate (name) { 38 | return readFileSync(join(__dirname, `${name}.pem`)) 39 | } 40 | 41 | function serve (request, response) { 42 | setTimeout(respond, 2) 43 | 44 | function respond () { 45 | const url = request.url 46 | const download = url === '/download' 47 | const upload = url === '/upload' 48 | const redirect = url === '/redirect' 49 | let data 50 | 51 | function sendResponse () { 52 | const ok = download || upload || redirect 53 | const headers = request.headers 54 | const httpVersion = request.httpVersion 55 | const method = request.method 56 | const responseHeaders = { test: 'ok' } 57 | if (redirect) { 58 | responseHeaders.location = `http://${request.headers.host}/download` 59 | } 60 | lastRequest = { data, headers, httpVersion, method } 61 | response.writeHead(ok 62 | ? redirect ? 302 : 200 63 | : url === '/' ? 204 : 404, responseHeaders) 64 | if (ok) { 65 | response.write('data') 66 | } 67 | response.end() 68 | } 69 | 70 | if (upload) { 71 | data = '' 72 | request 73 | .on('data', chunk => (data += chunk)) 74 | .on('end', sendResponse) 75 | } else { 76 | sendResponse() 77 | } 78 | } 79 | } 80 | 81 | function startServers () { 82 | const secureOptions = { 83 | key: readCertificate('key'), 84 | cert: readCertificate('cert') 85 | } 86 | return createServer(http, insecurePort) 87 | .then(createServer.bind(null, https, securePort, secureOptions)) 88 | .then(createServer.bind(null, http2, http2Port, secureOptions)) 89 | } 90 | 91 | function stopServers () { 92 | let server 93 | while ((server = servers.pop())) { 94 | server.close() 95 | } 96 | } 97 | 98 | function makeRequest (protocol, host, port, path, options) { 99 | const https = protocol === 'https' 100 | const url = `${protocol}://${host}:${port}${path || ''}` 101 | let credentials, headers, method, outputFile, failOnOutputFileError, 102 | followRedirects, returnResponse, includeHeaders, data, httpVersion, 103 | timeout, requestDelay, requestCount 104 | if (options) { 105 | if (options.username) { 106 | credentials = options 107 | } else if (options.method) { 108 | method = options.method 109 | } else if (options.outputFile) { 110 | outputFile = options.outputFile 111 | includeHeaders = options.includeHeaders 112 | failOnOutputFileError = options.failOnOutputFileError 113 | } else if (options.returnResponse) { 114 | returnResponse = true 115 | includeHeaders = options.includeHeaders 116 | } else if (options.includeHeaders) { 117 | includeHeaders = options.includeHeaders 118 | } else if (options.data) { 119 | data = options.data 120 | if (options.contentType) { 121 | headers = { 'content-type': options.contentType } 122 | } 123 | } else if (options.httpVersion) { 124 | httpVersion = options.httpVersion 125 | } else if (options.timeout) { 126 | timeout = options.timeout 127 | } else { 128 | headers = options 129 | } 130 | followRedirects = options.followRedirects 131 | requestDelay = options.requestDelay 132 | requestCount = options.requestCount 133 | } 134 | const rejectUnauthorized = false 135 | return nettime(https || options 136 | ? { 137 | url, 138 | credentials, 139 | data, 140 | failOnOutputFileError, 141 | followRedirects, 142 | headers, 143 | httpVersion, 144 | includeHeaders, 145 | method, 146 | outputFile, 147 | rejectUnauthorized, 148 | returnResponse, 149 | timeout, 150 | requestDelay, 151 | requestCount 152 | } 153 | : url) 154 | .then(checkRequest.bind(null, { 155 | url, 156 | httpVersion, 157 | returnResponse, 158 | includeHeaders, 159 | followRedirects, 160 | requestCount 161 | })) 162 | } 163 | 164 | function checkRequest (options, result) { 165 | let resultCount = 4 166 | if (options.returnResponse) { 167 | ++resultCount 168 | } 169 | if (options.includeHeaders) { 170 | ++resultCount 171 | } 172 | if (options.followRedirects) { 173 | ++resultCount 174 | ok(Array.isArray(result)) 175 | ok(result.length > 0) 176 | if (result[0].statusCode === 302) { 177 | strictEqual(result.length, 2) 178 | checkSingleRequest(result[0]) 179 | checkSingleRequest(result[1]) 180 | } else { 181 | strictEqual(result.length, 1) 182 | checkSingleRequest(result[0]) 183 | } 184 | } else if (options.requestCount) { 185 | ok(Array.isArray(result)) 186 | strictEqual(result.length, options.requestCount) 187 | for (const singleResult of result) { 188 | checkSingleRequest(singleResult) 189 | } 190 | } else { 191 | strictEqual(typeof result, 'object') 192 | checkSingleRequest(result) 193 | } 194 | return result 195 | 196 | function checkSingleRequest (result) { 197 | if (options.followRedirects) { 198 | ok(typeof result.url === 'string') 199 | } 200 | strictEqual(Object.keys(result).length, resultCount) 201 | const httpVersion = options.httpVersion || '1.1' 202 | if (httpVersion === '1.0') { 203 | result.httpVersion = '1.0' 204 | } 205 | strictEqual(result.httpVersion, httpVersion) 206 | strictEqual(lastRequest.httpVersion, httpVersion) 207 | const { timings } = result 208 | strictEqual(typeof timings, 'object') 209 | checkTiming(timings.socketOpen) 210 | const { tcpConnection, firstByte } = timings 211 | checkTiming(tcpConnection) 212 | checkTiming(firstByte) 213 | checkTiming(timings.contentTransfer) 214 | checkTiming(timings.socketClose) 215 | ok(getTestDuration(tcpConnection, firstByte) >= 1e6) 216 | } 217 | } 218 | 219 | function getTestDuration (start, end) { 220 | let seconds = end[0] - start[0] 221 | let nanoseconds = end[1] - start[1] 222 | if (nanoseconds < 0) { 223 | --seconds 224 | nanoseconds += 1e9 225 | } 226 | return seconds * 1e9 + nanoseconds 227 | } 228 | 229 | function checkTiming (timing) { 230 | ok(Array.isArray(timing)) 231 | strictEqual(timing.length, 2) 232 | strictEqual(typeof timing[0], 'number') 233 | strictEqual(typeof timing[1], 'number') 234 | } 235 | 236 | function checkNull (timing) { 237 | equal(timing, null) 238 | } 239 | 240 | strictEqual(typeof nettime, 'function') 241 | 242 | test('start testing servers', async () => { 243 | await startServers() 244 | }) 245 | 246 | test('test a full URL', async () => { 247 | const result = await makeRequest('http', 'user:pass@localhost', insecurePort, '?search#hash') 248 | strictEqual(result.statusCode, 404) 249 | strictEqual(result.statusMessage, 'Not Found') 250 | }) 251 | 252 | test('test a full URL without password', async () => { 253 | const result = await makeRequest('http', 'user@localhost', insecurePort, '?search#hash') 254 | strictEqual(result.statusCode, 404) 255 | strictEqual(result.statusMessage, 'Not Found') 256 | }) 257 | 258 | test('test two requests', async () => { 259 | const results = await makeRequest('http', ipAddress, insecurePort, '/', { 260 | outputFile: 'test.out', 261 | requestCount: 2 262 | }) 263 | ok(Array.isArray(results)) 264 | for (const result of results) { 265 | checkRequest({}, result) 266 | strictEqual(result.statusCode, 204) 267 | strictEqual(result.statusMessage, 'No Content') 268 | } 269 | }) 270 | 271 | test('test two requests with delay', async () => { 272 | const start = new Date().getTime() 273 | await makeRequest('http', ipAddress, insecurePort, '/', { 274 | requestCount: 2, 275 | requestDelay: 10 276 | }) 277 | const end = new Date().getTime() 278 | ok(start + 10 < end) 279 | }) 280 | 281 | test('test with a hostname', async () => { 282 | const result = await makeRequest('http', 'localhost', insecurePort) 283 | const timings = result.timings 284 | strictEqual(result.statusCode, 204) 285 | strictEqual(result.statusMessage, 'No Content') 286 | strictEqual(Object.keys(timings).length, 6) 287 | checkTiming(timings.dnsLookup) 288 | checkNull(timings.tlsHandshake) 289 | }) 290 | 291 | test('test with an IP address', async () => { 292 | const result = await makeRequest('http', ipAddress, insecurePort, '/download') 293 | const timings = result.timings 294 | strictEqual(result.statusCode, 200) 295 | strictEqual(result.statusMessage, 'OK') 296 | strictEqual(Object.keys(timings).length, 5) 297 | checkNull(timings.dnsLookup) 298 | checkNull(timings.tlsHandshake) 299 | }) 300 | 301 | test('test with the HTTPS protocol', async () => { 302 | const result = await makeRequest('https', ipAddress, securePort) 303 | const timings = result.timings 304 | strictEqual(result.statusCode, 204) 305 | strictEqual(Object.keys(timings).length, 6) 306 | checkNull(timings.dnsLookup) 307 | checkTiming(timings.tlsHandshake) 308 | }) 309 | 310 | test('test with a missing web page', async () => { 311 | const result = await makeRequest('http', ipAddress, insecurePort, '/missing') 312 | const timings = result.timings 313 | strictEqual(result.statusCode, 404) 314 | strictEqual(result.statusMessage, 'Not Found') 315 | strictEqual(Object.keys(timings).length, 5) 316 | checkNull(timings.dnsLookup) 317 | checkNull(timings.tlsHandshake) 318 | }) 319 | 320 | test('test failed connection to a not responding host', async () => { 321 | const start = new Date().getTime() 322 | try { 323 | await makeRequest('http', '192.0.2.1', 80, '/', { 324 | timeout: 10 325 | }) 326 | fail('not responding host') 327 | } catch (error) { 328 | const end = new Date().getTime() 329 | ok(start + 9 < end) 330 | ok(error instanceof Error) 331 | strictEqual(error.code, 'ETIMEDOUT') 332 | } 333 | }) 334 | 335 | test('test response timeout', async () => { 336 | try { 337 | await makeRequest('http', ipAddress, insecurePort, '', { 338 | timeout: 1 339 | }) 340 | fail('response timeout') 341 | } catch (error) { 342 | ok(error instanceof Error) 343 | strictEqual(error.code, 'ETIMEDOUT') 344 | } 345 | }) 346 | 347 | test('test with an invalid URL', async () => { 348 | try { 349 | await makeRequest('dummy', ipAddress, 1) 350 | fail('invalid URL') 351 | } catch (error) { 352 | ok(error instanceof Error) 353 | ok(error.message.indexOf('dummy:') > 0) 354 | } 355 | }) 356 | 357 | test('test with custom headers', async () => { 358 | await makeRequest('http', ipAddress, insecurePort, '/download', { 359 | TestHeader: 'Test value' 360 | }) 361 | const headers = lastRequest.headers 362 | ok(headers) 363 | strictEqual(Object.keys(headers).length, 3) 364 | strictEqual(headers.connection, 'close') 365 | strictEqual(headers.host, '127.0.0.1:8899') 366 | strictEqual(headers.testheader, 'Test value') 367 | }) 368 | 369 | test('test with credentials', async () => { 370 | await makeRequest('http', ipAddress, insecurePort, '/download', { 371 | username: 'guest', 372 | password: 'secret' 373 | }) 374 | const headers = lastRequest.headers 375 | ok(headers) 376 | strictEqual(Object.keys(headers).length, 3) 377 | strictEqual(headers.connection, 'close') 378 | strictEqual(headers.host, '127.0.0.1:8899') 379 | strictEqual(headers.authorization, 'Basic Z3Vlc3Q6c2VjcmV0') 380 | }) 381 | 382 | test('test with the HEAD verb', async () => { 383 | await makeRequest('http', ipAddress, insecurePort, '/download', { 384 | method: 'HEAD' 385 | }) 386 | strictEqual(lastRequest.method, 'HEAD') 387 | }) 388 | 389 | test('test returning of received data', async () => { 390 | const result = await makeRequest('http', ipAddress, insecurePort, '/download', { 391 | returnResponse: true 392 | }) 393 | const response = result.response 394 | ok(!result.headers) 395 | ok(response) 396 | strictEqual(response.length, 4) 397 | }) 398 | 399 | test('test returning of received data with headers', async () => { 400 | const result = await makeRequest('http', ipAddress, insecurePort, '/download', { 401 | returnResponse: true, 402 | includeHeaders: true 403 | }) 404 | const response = result.response 405 | const headers = result.headers 406 | ok(headers) 407 | strictEqual(headers.test, 'ok') 408 | ok(response) 409 | strictEqual(response.length, 4) 410 | }) 411 | 412 | test('test returning of received headers alone', async () => { 413 | const result = await makeRequest('http', ipAddress, insecurePort, '/download', { 414 | includeHeaders: true 415 | }) 416 | const headers = result.headers 417 | ok(headers) 418 | strictEqual(headers.test, 'ok') 419 | ok(!result.response) 420 | }) 421 | 422 | test('test writing an output file', async () => { 423 | await makeRequest('http', ipAddress, insecurePort, '/download', { 424 | outputFile: 'test.out' 425 | }) 426 | const stat = statSync('test.out') 427 | ok(stat) 428 | strictEqual(stat.size, 4) 429 | unlinkSync('test.out') 430 | }) 431 | 432 | test('test writing an output file with headers', async () => { 433 | await makeRequest('http', ipAddress, insecurePort, '/download', { 434 | outputFile: 'test.out', 435 | includeHeaders: true 436 | }) 437 | const stat = statSync('test.out') 438 | ok(stat) 439 | ok(stat.size > 4) 440 | unlinkSync('test.out') 441 | }) 442 | 443 | test('test failure when writing to an output file', async () => { 444 | try { 445 | await makeRequest('http', ipAddress, insecurePort, '/download', { 446 | outputFile: '/' 447 | }) 448 | fail('writing an output file') 449 | } catch (error) { 450 | ok(error instanceof Error) 451 | strictEqual(error.code, 'EISDIR') 452 | } 453 | }) 454 | 455 | test('test an ignored failure when writing to an output file', async () => { 456 | await makeRequest('http', ipAddress, insecurePort, '/download', { 457 | outputFile: '/', 458 | failOnOutputFileError: false 459 | }) 460 | strictEqual(process.exitCode, 2) 461 | process.exitCode = 0 462 | }) 463 | 464 | test('test posting data', async () => { 465 | await makeRequest('http', ipAddress, insecurePort, '/upload', { 466 | data: 'test=ok' 467 | }) 468 | strictEqual(lastRequest.method, 'POST') 469 | strictEqual(lastRequest.data, 'test=ok') 470 | }) 471 | 472 | test('test posting data with content type', async () => { 473 | await makeRequest('http', ipAddress, insecurePort, '/upload', { 474 | data: 'test=ok', 475 | contentType: 'application/x-www-form-urlencoded' 476 | }) 477 | strictEqual(lastRequest.method, 'POST') 478 | strictEqual(lastRequest.data, 'test=ok') 479 | }) 480 | 481 | test('test not followed redirect', async () => { 482 | const result = await makeRequest('http', ipAddress, insecurePort, '/redirect') 483 | strictEqual(result.statusCode, 302) 484 | }) 485 | 486 | test('test followed redirect', async () => { 487 | const result = await makeRequest('http', ipAddress, insecurePort, '/redirect', { 488 | followRedirects: true 489 | }) 490 | ok(Array.isArray(result)) 491 | strictEqual(result.length, 2) 492 | strictEqual(result[0].statusCode, 302) 493 | strictEqual(result[1].statusCode, 200) 494 | }) 495 | 496 | test('test no redirection with following redirect enabled', async () => { 497 | const result = await makeRequest('http', ipAddress, insecurePort, '/download', { 498 | followRedirects: true 499 | }) 500 | ok(Array.isArray(result)) 501 | strictEqual(result.length, 1) 502 | strictEqual(result[0].statusCode, 200) 503 | }) 504 | 505 | test('test HTTP 1.0', async () => { 506 | await makeRequest('http', ipAddress, insecurePort, '/download', { 507 | httpVersion: '1.0' 508 | }) 509 | }) 510 | 511 | if (http2) { 512 | test('test HTTP 2.0 with the http scheme', async () => { 513 | try { 514 | await makeRequest('http', ipAddress, http2Port, '/download', { 515 | httpVersion: '2.0' 516 | }) 517 | fail('HTTP 2.0 with the http scheme') 518 | } catch (error) { 519 | ok(error instanceof Error) 520 | strictEqual(error.code, 'ERR_INSECURE_SCHEME') 521 | } 522 | }) 523 | 524 | test('test HTTP 2.0 with the https scheme', async () => { 525 | await makeRequest('https', ipAddress, http2Port, '/download', { 526 | httpVersion: '2.0' 527 | }) 528 | }) 529 | } 530 | 531 | test('stop testing servers', () => { 532 | stopServers() 533 | }) 534 | 535 | test('test carried exported methods', () => { 536 | strictEqual(nettime.nettime, nettime) 537 | strictEqual(nettime.getDuration, getDuration) 538 | strictEqual(nettime.getMilliseconds, getMilliseconds) 539 | strictEqual(nettime.isRedirect, isRedirect) 540 | }) 541 | -------------------------------------------------------------------------------- /tests/printer.js: -------------------------------------------------------------------------------- 1 | import tehanu from 'tehanu' 2 | import { ok } from 'assert' 3 | import { printTimings } from '../lib/printer.js' 4 | 5 | const test = tehanu(import.meta.url) 6 | 7 | const example = { 8 | timings: { 9 | socketOpen: [0, 15936126], 10 | dnsLookup: [0, 16554700], 11 | tcpConnection: [0, 29498118], 12 | tlsHandshake: [0, 75912898], 13 | firstByte: [0, 166826235], 14 | contentTransfer: [0, 208424267], 15 | socketClose: [0, 209561300] 16 | }, 17 | httpVersion: '1.1', 18 | statusCode: 200, 19 | statusMessage: 'OK' 20 | } 21 | const { timings } = example 22 | 23 | test('test printing seconds', () => { 24 | const output = printTimings(timings, 's') 25 | ok(/\ds/.test(output), 'seconds') 26 | ok(!/\dms/.test(output), 'not milliseconds') 27 | }) 28 | 29 | test('test printing seconds and nanoseconds', () => { 30 | const output = printTimings(timings, 's+ns') 31 | ok(/\ds/.test(output), 'seconds') 32 | ok(/\dms/.test(output), 'milliseconds') 33 | }) 34 | 35 | test('test printing seconds with incomplete timing', () => { 36 | const timings2 = timings 37 | delete timings2.tlsHandshake 38 | printTimings(timings2, 's') 39 | }) 40 | 41 | test('test printing nanoseconds with incomplete timing', () => { 42 | const timings2 = timings 43 | delete timings2.tlsHandshake 44 | printTimings(timings2, 's+ns') 45 | }) 46 | -------------------------------------------------------------------------------- /tests/timings.js: -------------------------------------------------------------------------------- 1 | import tehanu from 'tehanu' 2 | import { deepStrictEqual, fail } from 'assert' 3 | import { 4 | events, getDuration, getMilliseconds, computeAverageDurations, createTimingsFromDurations 5 | } from '../lib/timings.js' 6 | 7 | const test = tehanu(import.meta.url) 8 | 9 | const eventCopy = { 10 | socketOpen: 'Socket Open', 11 | dnsLookup: 'DNS Lookup', 12 | tcpConnection: 'TCP Connection', 13 | tlsHandshake: 'TLS Handshake', 14 | firstByte: 'First Byte', 15 | contentTransfer: 'Content Transfer', 16 | socketClose: 'Socket Close' 17 | } 18 | 19 | test('test timing event names', () => { 20 | deepStrictEqual(Object.keys(events), Object.keys(eventCopy)) 21 | deepStrictEqual(Object.values(events), Object.values(eventCopy)) 22 | }) 23 | 24 | test('test getting duration', () => { 25 | deepStrictEqual(getDuration([0, 100], [0, 200]), [0, 100]) 26 | deepStrictEqual(getDuration([0, 100], [1, 200]), [1, 100]) 27 | deepStrictEqual(getDuration([0, 200], [1, 100]), [0, 999999900]) 28 | }) 29 | 30 | test('test getting milliseconds', () => { 31 | deepStrictEqual(getMilliseconds([0, 1e6]), 1) 32 | deepStrictEqual(getMilliseconds([1, 1000]), 1000.001) 33 | }) 34 | 35 | test('test computing average durations', () => { 36 | const input = [ 37 | { 38 | socketOpen: [0, 100], 39 | tcpConnection: [0, 300] 40 | }, 41 | { 42 | socketOpen: [0, 100], 43 | tcpConnection: [1, 100] 44 | } 45 | ] 46 | const expected = { 47 | socketOpen: [0, 100], 48 | dnsLookup: undefined, 49 | tcpConnection: [0.5, 100], 50 | tlsHandshake: undefined, 51 | firstByte: undefined, 52 | contentTransfer: undefined, 53 | socketClose: undefined 54 | } 55 | const actual = computeAverageDurations(input) 56 | deepStrictEqual(actual, expected) 57 | }) 58 | 59 | test('test checking unexpected defined event timings', () => { 60 | const input = [ 61 | { 62 | socketOpen: [0, 100], 63 | tcpConnection: [0, 300] 64 | }, 65 | { 66 | socketOpen: [0, 100], 67 | dnsLookup: [0, 500], 68 | tcpConnection: [1, 100] 69 | } 70 | ] 71 | try { 72 | computeAverageDurations(input) 73 | fail('unexpected event timings') 74 | } catch { 75 | /* ignored */ 76 | } 77 | }) 78 | 79 | test('test checking unexpected undefined event timings', () => { 80 | const input = [ 81 | { 82 | socketOpen: [0, 100], 83 | dnsLookup: [0, 500], 84 | tcpConnection: [0, 300] 85 | }, 86 | { 87 | socketOpen: [0, 100], 88 | tcpConnection: [1, 100] 89 | } 90 | ] 91 | try { 92 | computeAverageDurations(input) 93 | fail('unexpected undefined event timings') 94 | } catch { 95 | /* ignored */ 96 | } 97 | }) 98 | 99 | test('test creating timings from durations', () => { 100 | const input = { 101 | socketOpen: [0, 100], 102 | dnsLookup: undefined, 103 | tcpConnection: [0.5, 100], 104 | tlsHandshake: undefined, 105 | firstByte: undefined, 106 | contentTransfer: undefined, 107 | socketClose: undefined 108 | } 109 | const expected = { 110 | socketOpen: [0, 100], 111 | tcpConnection: [0.5, 200] 112 | } 113 | const actual = createTimingsFromDurations(input) 114 | deepStrictEqual(actual, expected) 115 | }) 116 | -------------------------------------------------------------------------------- /tests/types.ts: -------------------------------------------------------------------------------- 1 | import nettime, { 2 | nettime as nettime2, getDuration, getMilliseconds, isRedirect, 3 | NettimeResponse 4 | } from '../lib/nettime.js' 5 | 6 | const _r1: Promise = nettime('') 7 | const _r2: Promise = nettime2({ url: '' }) 8 | const _d1: number = getDuration(1, 2) 9 | const _m1: number = getMilliseconds([1]) 10 | const _i1: boolean = isRedirect(1) 11 | --------------------------------------------------------------------------------