├── .editorconfig ├── .eslintrc.yaml ├── .gitattributes ├── .github └── workflows │ └── ci.yaml ├── .gitignore ├── .npmrc ├── LICENSE ├── Makefile ├── README.md ├── index.js ├── index.test.js ├── package-lock.json ├── package.json ├── tcpie.js ├── updates.config.js └── vitest.config.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | tab_width = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [Makefile] 13 | indent_style = tab 14 | -------------------------------------------------------------------------------- /.eslintrc.yaml: -------------------------------------------------------------------------------- 1 | root: true 2 | extends: silverwind 3 | 4 | ignorePatterns: 5 | - /dist/ 6 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | test: 6 | strategy: 7 | fail-fast: false 8 | matrix: 9 | node: [18, 20] 10 | os: [ubuntu-latest, macos-latest, windows-latest] 11 | 12 | runs-on: ${{matrix.os}} 13 | steps: 14 | - uses: actions/checkout@v4 15 | - uses: actions/setup-node@v4 16 | with: 17 | node-version: ${{matrix.node}} 18 | - run: make test 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.vscode 2 | /dist 3 | /node_modules 4 | /npm-debug.log* 5 | /yarn-error.log 6 | /yarn.lock 7 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | audit=false 2 | fund=false 3 | package-lock=true 4 | save-exact=true 5 | update-notifier=false 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) silverwind 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 17 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 18 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 19 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 20 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 21 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 22 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SRC := tcpie.js 2 | DST := dist/tcpie.js 3 | 4 | node_modules: package-lock.json 5 | npm install --no-save 6 | @touch node_modules 7 | 8 | .PHONY: deps 9 | deps: node_modules 10 | 11 | .PHONY: lint 12 | lint: node_modules 13 | npx eslint --color --quiet . 14 | 15 | .PHONY: lint-fix 16 | lint-fix: node_modules 17 | npx eslint --color --quiet . --fix 18 | 19 | .PHONY: test 20 | test: lint build node_modules 21 | npx vitest 22 | 23 | .PHONY: build 24 | build: $(DST) 25 | 26 | $(DST): $(SRC) node_modules 27 | # workaround for https://github.com/evanw/esbuild/issues/1921 28 | npx esbuild --log-level=warning --platform=node --target=node16 --format=esm --bundle --minify --legal-comments=none --banner:js="import {createRequire} from 'module';const require = createRequire(import.meta.url);" --define:import.meta.VERSION=\"$(shell jq .version package.json)\" --outfile=$(DST) $(SRC) 29 | chmod +x $(DST) 30 | 31 | .PHONY: publish 32 | publish: 33 | git push -u --tags origin master 34 | npm publish 35 | 36 | .PHONY: update 37 | update: node_modules 38 | npx updates -cu 39 | rm -rf node_modules package-lock.json 40 | npm install 41 | @touch node_modules 42 | 43 | .PHONY: patch 44 | patch: node_modules test 45 | npx versions -c 'make --no-print-directory build' patch package.json package-lock.json 46 | @$(MAKE) --no-print-directory publish 47 | 48 | .PHONY: minor 49 | minor: node_modules test 50 | npx versions -c 'make --no-print-directory build' minor package.json package-lock.json 51 | @$(MAKE) --no-print-directory publish 52 | 53 | .PHONY: major 54 | major: node_modules test 55 | npx versions -c 'make --no-print-directory build' major package.json package-lock.json 56 | @$(MAKE) --no-print-directory publish 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tcpie 2 | [![](https://img.shields.io/npm/v/tcpie.svg?style=flat)](https://www.npmjs.org/package/tcpie) [![](https://img.shields.io/npm/dm/tcpie.svg)](https://www.npmjs.org/package/tcpie) [![](https://img.shields.io/bundlephobia/minzip/tcpie.svg)](https://bundlephobia.com/package/tcpie) [![](https://packagephobia.com/badge?p=tcpie)](https://packagephobia.com/result?p=tcpie) 3 | 4 | > Ping any TCP port 5 | 6 | tcpie is a tool to measure latency and verify the reliabilty of a TCP connection. It does so by initiating a handshake followed by an immediately termination of the socket. While many existing tools require raw socket access, tcpie runs fine in user space. An API for use as a module is also provided. 7 | 8 | ## CLI 9 | 10 | ### Installation 11 | ``` 12 | $ npm i -g tcpie 13 | ``` 14 | ### Example 15 | ``` 16 | $ tcpie -c 5 google.com 443 17 | TCPIE google.com (188.21.9.120) port 443 18 | connected to google.com:443 seq=1 srcport=59053 time=12.9 ms 19 | connected to google.com:443 seq=2 srcport=59054 time=10.0 ms 20 | connected to google.com:443 seq=3 srcport=59055 time=10.1 ms 21 | connected to google.com:443 seq=4 srcport=59056 time=11.4 ms 22 | connected to google.com:443 seq=5 srcport=59057 time=10.4 ms 23 | 24 | --- google.com tcpie statistics --- 25 | 5 handshakes attempted, 5 succeeded, 0% failed 26 | rtt min/avg/max/stdev = 10.012/10.970/12.854/1.190 ms 27 | ``` 28 | 29 | ## API 30 | 31 | ### Usage 32 | ```js 33 | import {tcpie} from "tcpie"; 34 | const pie = tcpie("google.com", 443, {count: 10, interval: 500, timeout: 2000}); 35 | 36 | pie.on("connect", function(stats) { 37 | console.info("connect", stats); 38 | }).on("error", function(err, stats) { 39 | console.error(err, stats); 40 | }).on("timeout", function(stats) { 41 | console.info("timeout", stats); 42 | }).on("end", function(stats) { 43 | console.info(stats); 44 | // -> { 45 | // -> sent: 10, 46 | // -> success: 10, 47 | // -> failed: 0, 48 | // -> target: { host: "google.com", port: 443 } 49 | // -> } 50 | }).start(); 51 | ``` 52 | #### tcpie(host, [port], [options]) 53 | - `host` *string* : the destination host name or IP address. Required. 54 | - `port` *number* : the destination port. Default: `22`. 55 | - `opts` *object* : options for count, interval and timeout. Defaults: `Infinity`, `1000`, `3000`. 56 | 57 | #### tcpie#start() 58 | Start connecting 59 | 60 | #### tcpie#stop() 61 | Stops connecting 62 | 63 | #### *options* object 64 | - `count` *number* : the number of connection attempts in milliseconds (default: Infinity). 65 | - `interval` *number* : the interval between connection attempts in milliseconds (default: 1000). 66 | - `timeout` *number* : the connection timeout in milliseconds (default: 3000). 67 | 68 | #### Events 69 | - `connect` : Arguments: `stats`. Connection attempt succeeded. 70 | - `timeout` : Arguments: `stats`. Connection attempt ran into the timeout. 71 | - `error` : Arguments: `err`, `stats`. Connection attempt failed. 72 | - `end` : Arguments: `stats`. All connection attempts have finished. 73 | 74 | #### *stats* argument properties 75 | - `sent` *number* : number of total attempts made. 76 | - `success` *number* : number of successfull attempts. 77 | - `failed` *number* : number of failed attempts. 78 | - `target` *object* : target details: `host` and `port`. 79 | 80 | The following properties are present on all events except `end`: 81 | - `rtt` *number* : roundtrip time in milliseconds. *undefined* if failed. 82 | - `socket` *object* : socket details: `localAddress`, `localPort`, `remoteAddress`, `remotePort`. 83 | 84 | © [silverwind](https://github.com/silverwind), distributed under BSD licence 85 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import {EventEmitter} from "node:events"; 2 | import {Socket} from "node:net"; 3 | import {inherits} from "node:util"; 4 | 5 | const Tcpie = function(host, port, opts) { 6 | if (!(this instanceof Tcpie)) return new Tcpie(); 7 | if (typeof host !== "string") throw new Error("host is required"); 8 | if (port === undefined) port = 80; 9 | 10 | this.host = host; 11 | this.port = port; 12 | 13 | this.opts = {interval: 1000, 14 | timeout: 3000, 15 | count: Infinity, ...opts}; 16 | 17 | this.stats = { 18 | sent: 0, 19 | success: 0, 20 | failed: 0 21 | }; 22 | }; 23 | 24 | inherits(Tcpie, EventEmitter); 25 | 26 | Tcpie.prototype.start = function start(subsequent) { 27 | if (!subsequent) { 28 | this.stats.sent = 0; 29 | this.stats.success = 0; 30 | this.stats.failed = 0; 31 | } 32 | 33 | this._next = setTimeout(start.bind(this, true), this.opts.interval); 34 | this._done = false; 35 | this._abort = false; 36 | this._socket = new Socket(); 37 | this._startTime = performance.now(); 38 | 39 | this._socket.setTimeout(this.opts.timeout); 40 | this._socket.on("timeout", () => { 41 | if (!this._done) { 42 | this._done = true; 43 | this.stats.sent++; 44 | this.stats.failed++; 45 | this.emit("timeout", addDetails(this, this)); 46 | this._socket.destroy(); 47 | checkEnd(this); 48 | } 49 | }); 50 | 51 | this._socket.on("error", err => { 52 | if (!this._done) { 53 | this._done = true; 54 | this.stats.sent++; 55 | this.stats.failed++; 56 | this.emit("error", err, addDetails(this, this)); 57 | this._socket.destroy(); 58 | checkEnd(this); 59 | } 60 | }); 61 | 62 | this._socket.connect(this.port, this.host, () => { 63 | if (!this._done) { 64 | this._done = true; 65 | this.stats.sent++; 66 | this.stats.success++; 67 | this.stats.rtt = (performance.now() - this._startTime); 68 | this.emit("connect", addDetails(this, this)); 69 | this._socket.end(); 70 | checkEnd(this); 71 | } 72 | }); 73 | 74 | return this; 75 | }; 76 | 77 | Tcpie.prototype.stop = function stop() { 78 | this._abort = true; 79 | this._socket.end(); 80 | checkEnd(this); 81 | return this; 82 | }; 83 | 84 | export function tcpie(...args) { 85 | return new Tcpie(...args); 86 | } 87 | 88 | // add details to stats object 89 | function addDetails(that, socket) { 90 | const ret = that.stats; 91 | 92 | ret.target = { 93 | host: that.host, 94 | port: that.port 95 | }; 96 | 97 | ret.socket = { 98 | localAddress: socket.localAddress, 99 | localPort: socket.localPort, 100 | remoteAddress: socket.remoteAddress, 101 | remotePort: socket.remotePort 102 | }; 103 | 104 | return ret; 105 | } 106 | 107 | // check end condition 108 | function checkEnd(that) { 109 | if (that._abort || ((that.stats.failed + that.stats.success) >= that.opts.count)) { 110 | if (that._next) clearTimeout(that._next); 111 | 112 | that.emit("end", { 113 | sent: that.stats.sent, 114 | success: that.stats.success, 115 | failed: that.stats.failed, 116 | target: { 117 | host: that.host, 118 | port: that.port 119 | } 120 | }); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /index.test.js: -------------------------------------------------------------------------------- 1 | import {tcpie} from "./index.js"; 2 | 3 | test("first", () => { 4 | let runs = 0; 5 | const pie = tcpie("google.com", 443, {count: 2}); 6 | pie.on("end", stats => { 7 | runs++; 8 | expect(stats.sent).toEqual(2); 9 | expect(stats.success).toEqual(2); 10 | expect(stats.failed).toEqual(0); 11 | if (runs < 5) pie.start(); // run 5 times 12 | }).start(); 13 | }); 14 | 15 | test("second", () => { 16 | const pie = tcpie("google.com", 443, {count: 2}); 17 | pie.on("connect", stats => { 18 | expect(stats.sent).toEqual(1); 19 | expect(stats.success).toEqual(1); 20 | expect(stats.failed).toEqual(0); 21 | pie.stop(); 22 | }).on("end", stats => { 23 | expect(stats.sent).toEqual(1); 24 | expect(stats.success).toEqual(1); 25 | expect(stats.failed).toEqual(0); 26 | }).start(); 27 | }); 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tcpie", 3 | "version": "11.0.1", 4 | "description": "Ping any TCP port", 5 | "author": "silverwind ", 6 | "repository": "silverwind/tcpie", 7 | "license": "BSD-2-Clause", 8 | "exports": "./index.js", 9 | "bin": "./dist/tcpie.js", 10 | "type": "module", 11 | "sideEffects": false, 12 | "engines": { 13 | "node": ">=18" 14 | }, 15 | "files": [ 16 | "./index.js", 17 | "./dist/tcpie.js" 18 | ], 19 | "dependencies": { 20 | "compute-stdev": "1.0.0", 21 | "glowie": "1.3.2", 22 | "minimist": "1.2.8", 23 | "supports-color": "10.0.0" 24 | }, 25 | "devDependencies": { 26 | "eslint": "8.57.0", 27 | "eslint-config-silverwind": "99.0.0", 28 | "typescript": "5.7.3", 29 | "typescript-config-silverwind": "7.0.0", 30 | "updates": "16.4.2", 31 | "versions": "12.1.3", 32 | "vitest": "3.0.5", 33 | "vitest-config-silverwind": "10.0.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tcpie.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | import {red, yellow, green, disableColor} from "glowie"; 3 | import {isIP} from "node:net"; 4 | import {lookup} from "node:dns"; 5 | import process, {exit, argv, stdin, stdout, stderr} from "node:process"; 6 | import stdev from "compute-stdev"; 7 | import {tcpie} from "./index.js"; 8 | import minimist from "minimist"; 9 | import supportsColor from "supports-color"; 10 | 11 | const args = minimist(argv.slice(2), { 12 | boolean: [ 13 | "color", "C", 14 | "timestamp", "T", 15 | "flood", "f", 16 | "version", "v" 17 | ] 18 | }); 19 | 20 | const packageVersion = import.meta.VERSION || "0.0.0"; 21 | const DIGITS_LINE = 1; 22 | const DIGITS_STATS = 3; 23 | const DIGITS_PERC = 0; 24 | const DEFAULT_PORT = 22; 25 | 26 | const usage = [ 27 | "", 28 | " Usage: tcpie [options] host[:port] [port|22]", 29 | "", 30 | " Options:", 31 | "", 32 | " -v, --version output version", 33 | " -c, --count number of connects (default: infinite)", 34 | " -i, --interval wait n seconds between connects (default: 1)", 35 | " -t, --timeout connection timeout in seconds (default: 3)", 36 | " -T, --timestamp add timestamps to output", 37 | " -f, --flood flood mode, connect as fast as possible", 38 | " -C, --no-color disable color output", 39 | "", 40 | " Examples:", 41 | "", 42 | " $ tcpie google.com", 43 | " $ tcpie -i .1 8.8.8.8:53", 44 | " $ tcpie -c5 -t.05 aspmx.l.google.com 25", 45 | "", 46 | "" 47 | ].join("\n"); 48 | 49 | if (args.v) { 50 | console.info(packageVersion); 51 | exit(0); 52 | } 53 | 54 | if (!args._.length || args._.length > 2 || (args._[1] && Number.isNaN(parseInt(args._[1])))) { 55 | help(); 56 | } 57 | 58 | let host = args._[0]; 59 | const opts = {}; 60 | let port = parseInt(args._[1]); 61 | let printed = false; 62 | const rtts = []; 63 | let stats; 64 | 65 | if (typeof host !== "string") { 66 | help(); 67 | } 68 | 69 | // host:port syntax 70 | const matches = /^(.+):(\d+)$/.exec(host); 71 | if (matches && matches.length === 3 && !port) { 72 | host = matches[1]; 73 | port = matches[2]; 74 | } 75 | 76 | if (!port) port = DEFAULT_PORT; 77 | if (args.count || args.c) opts.count = parseInt(args.count || args.c); 78 | if (args.interval || args.i) opts.interval = secondsToMs(args.interval || args.i); 79 | if (args.timeout || args.t) opts.timeout = secondsToMs(args.timeout || args.t); 80 | if (args.flood || args.f) opts.interval = 0; 81 | if (args.C || !supportsColor.stdout) disableColor(); 82 | 83 | // Do a DNS lookup and start the connects 84 | if (!isIP(host)) { 85 | lookup(host, (err, address) => { 86 | if (!err) { 87 | printStart(host, address, port); 88 | run(host, port, opts); 89 | } else { 90 | if (err.code === "ENOTFOUND") writeLine(red("ERROR:"), `Host '${host}' not found`); 91 | else writeLine(red("ERROR:"), err.code, err.syscall || ""); 92 | exit(1); 93 | } 94 | }); 95 | } else { 96 | printStart(host, host, port); 97 | run(host, port, opts); 98 | } 99 | 100 | function run(host, port, opts) { 101 | const pie = tcpie(host, port, opts); 102 | 103 | pie.on("error", (err, data) => { 104 | stats = data; 105 | writeLine( 106 | red("error connecting to", `${data.target.host}:${data.target.port}`), 107 | `seq=${data.sent}`, 108 | `error=${red(err.code)}` 109 | ); 110 | }).on("connect", data => { 111 | stats = data; 112 | rtts.push(data.rtt); 113 | writeLine( 114 | green("connected to", `${data.target.host}:${data.target.port}`), 115 | `seq=${data.sent}`, 116 | (data.socket.localPort !== undefined) ? `srcport=${data.socket.localPort}` : "", 117 | `time=${colorRTT(data.rtt.toFixed(DIGITS_LINE))}`, 118 | ); 119 | }).on("timeout", data => { 120 | stats = data; 121 | writeLine( 122 | red("timeout connecting to", `${data.target.host}:${data.target.port}`), 123 | `seq=${data.sent}`, 124 | data.socket.localPort && `srcport=${data.socket.localPort}` 125 | ); 126 | }); 127 | 128 | if (stdin.isTTY) { 129 | stdin.setRawMode(true); 130 | stdin.on("data", bytes => { 131 | // http://nemesis.lonestar.org/reference/telecom/codes/ascii.html 132 | const exitCodes = [ 133 | 3, // SIGINT 134 | 4, // EOF 135 | 26, // SIGTSTP 136 | 28, // SIGQUIT 137 | ]; 138 | for (const byte of bytes) { 139 | if (exitCodes.includes(byte)) printEnd(); 140 | } 141 | }); 142 | } else { 143 | process.on("SIGINT", exit); 144 | process.on("SIGQUIT", exit); 145 | process.on("SIGTERM", exit); 146 | process.on("SIGTSTP", exit); 147 | } 148 | 149 | process.on("exit", printEnd); 150 | pie.on("end", printEnd).start(); 151 | } 152 | 153 | function printStart(host, address, port) { 154 | writeLine("TCPIE", host, `(${address})`, "port", String(port)); 155 | } 156 | 157 | function printEnd() { 158 | let sum = 0, min = Infinity, max = 0, avg, dev; 159 | 160 | if (printed) exit(stats.success === 0 && 1 || 0); 161 | 162 | if (stats && stats.sent > 0) { 163 | for (const rtt of rtts) { 164 | if (rtt <= min) min = rtt.toFixed(DIGITS_STATS); 165 | if (rtt >= max) max = rtt.toFixed(DIGITS_STATS); 166 | sum += rtt; 167 | } 168 | 169 | avg = (sum / rtts.length).toFixed(DIGITS_STATS); 170 | dev = stdev(rtts).toFixed(DIGITS_STATS); 171 | 172 | if (min === Infinity) min = "0"; 173 | if (Number.isNaN(avg)) avg = "0"; 174 | 175 | printed = true; 176 | 177 | writeLine( 178 | "\n---", host, `tcpie statistics`, "---", 179 | `\n${stats.sent}`, "handshakes attempted,", stats.success || "0", "succeeded,", 180 | `${((stats.failed / stats.sent) * 100).toFixed(DIGITS_PERC)}% failed`, 181 | "\nrtt min/avg/max/stdev =", `${min}/${avg}/${max}/${dev}`, "ms" 182 | ); 183 | 184 | exit(stats.success === 0 && 1 || 0); 185 | } else { 186 | exit(1); 187 | } 188 | } 189 | 190 | function colorRTT(rtt) { 191 | if (rtt >= 150) { 192 | return `${red(rtt)} ms`; 193 | } else if (rtt >= 75) { 194 | return `${yellow(rtt)} ms`; 195 | } else { 196 | return `${green(rtt)} ms`; 197 | } 198 | } 199 | 200 | function writeLine(...arg) { 201 | arg = arg.filter(Boolean); 202 | if ((args.timeout || args.T) && arg[0][0] !== "\n") arg.unshift(timestamp()); 203 | arg.push("\n"); 204 | const stream = (stdout._type === "pipe" && printed) ? stderr : stdout; 205 | stream.write(arg.join(" ")); 206 | } 207 | 208 | function help() { 209 | stdout.write(usage); 210 | exit(1); 211 | } 212 | 213 | function secondsToMs(s) { 214 | return (parseFloat(s) * 1000); 215 | } 216 | 217 | function timestamp() { 218 | const now = new Date(); 219 | const year = now.getFullYear(); 220 | let month = now.getMonth() + 1; 221 | let day = now.getDate(); 222 | let hrs = now.getHours(); 223 | let mins = now.getMinutes(); 224 | let secs = now.getSeconds(); 225 | 226 | if (month < 10) month = `0${month}`; 227 | if (day < 10) day = `0${day}`; 228 | if (hrs < 10) hrs = `0${hrs}`; 229 | if (mins < 10) mins = `0${mins}`; 230 | if (secs < 10) secs = `0${secs}`; 231 | return `${year}-${month}-${day} ${hrs}:${mins}:${secs}`; 232 | } 233 | -------------------------------------------------------------------------------- /updates.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | exclude: [ 3 | "eslint", // migrate to flat config first 4 | ], 5 | }; 6 | -------------------------------------------------------------------------------- /vitest.config.js: -------------------------------------------------------------------------------- 1 | import {defineConfig} from "vitest/config"; 2 | import {backend} from "vitest-config-silverwind"; 3 | 4 | export default defineConfig(backend({ 5 | url: import.meta.url, 6 | })); 7 | --------------------------------------------------------------------------------