├── .eslintignore ├── .eslintrc ├── .github └── workflows │ ├── nodejs-14.yml │ ├── nodejs.yml │ └── release.yml ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── package.json ├── src ├── bin │ └── detect-port.ts ├── detect-port.ts ├── index.ts └── wait-port.ts ├── test ├── cli.test.ts ├── demo │ ├── detect.js │ └── server.js ├── detect-port.test.ts └── wait-port.test.ts └── tsconfig.json /.eslintignore: -------------------------------------------------------------------------------- 1 | test/fixtures 2 | logs 3 | run 4 | coverage 5 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint-config-egg/typescript", 4 | "eslint-config-egg/lib/rules/enforce-node-prefix" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.github/workflows/nodejs-14.yml: -------------------------------------------------------------------------------- 1 | name: Node.js 14 CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Use Node.js 16 | uses: irby/setup-node-nvm@master 17 | with: 18 | node-version: '16.x' 19 | - run: npm install 20 | - run: npm run prepublishOnly 21 | - run: node -v 22 | - run: . /home/runner/mynvm/nvm.sh && nvm install 14 && nvm use 14 && node -v && node dist/commonjs/bin/detect-port.js 23 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | Job: 11 | name: Node.js 12 | uses: node-modules/github-actions/.github/workflows/node-test.yml@master 13 | with: 14 | os: 'ubuntu-latest' 15 | version: '16, 18, 20, 22' 16 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | 7 | jobs: 8 | release: 9 | name: Node.js 10 | uses: node-modules/github-actions/.github/workflows/node-release.yml@master 11 | secrets: 12 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 13 | GIT_TOKEN: ${{ secrets.GIT_TOKEN }} 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | *.un~ 4 | *.sw* 5 | .tshy* 6 | dist/ 7 | package-lock.json 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [2.1.0](https://github.com/node-modules/detect-port/compare/v2.0.1...v2.1.0) (2024-12-10) 4 | 5 | 6 | ### Features 7 | 8 | * refactor with async/await instead of callback ([#59](https://github.com/node-modules/detect-port/issues/59)) ([9254c18](https://github.com/node-modules/detect-port/commit/9254c18015498bea3617a609c13b8514b440c821)) 9 | 10 | ## [2.0.1](https://github.com/node-modules/detect-port/compare/v2.0.0...v2.0.1) (2024-12-08) 11 | 12 | 13 | ### Bug Fixes 14 | 15 | * use setTimeout Promise ([#58](https://github.com/node-modules/detect-port/issues/58)) ([db3ce1b](https://github.com/node-modules/detect-port/commit/db3ce1bd7c29bb5e6fcb60de1bccec0ef61d306f)) 16 | 17 | ## [2.0.0](https://github.com/node-modules/detect-port/compare/v1.6.1...v2.0.0) (2024-12-08) 18 | 19 | 20 | ### ⚠ BREAKING CHANGES 21 | 22 | * Drop Node.js < 16 support 23 | 24 | 1. 使用 ts 重构 25 | 2. 使用 tshy 支持 esm 和 cjs 26 | 3. test 使用 test-runner (这里需要 node v18 版本) 27 | 28 | merge from https://github.com/node-modules/detect-port/pull/51 29 | 30 | 32 | ## Summary by CodeRabbit 33 | 34 | - **New Features** 35 | - Introduced a new `waitPort` function to asynchronously wait for a 36 | specified port to become available. 37 | - Added a new ESLint configuration to enforce TypeScript linting rules. 38 | 39 | - **Bug Fixes** 40 | - Reverted a feature in the `detect-port` package due to issues raised. 41 | 42 | - **Documentation** 43 | - Updated `README.md` for improved clarity and updated badge links. 44 | - Modified `CONTRIBUTING.md` to reflect changes in testing commands. 45 | 46 | - **Chores** 47 | - Introduced a new TypeScript configuration file (`tsconfig.json`). 48 | - Updated `package.json` to reflect changes in dependencies and project 49 | structure. 50 | 51 | - **Tests** 52 | - Added comprehensive tests for the new `waitPort` and updated tests for 53 | the CLI and `detectPort` function. 54 | 55 | 56 | ### Features 57 | 58 | * refactor with typescript to support esm and cjs both ([#56](https://github.com/node-modules/detect-port/issues/56)) ([b5d32d2](https://github.com/node-modules/detect-port/commit/b5d32d2422bd753a87ff2e995514ac41f1c85346)) 59 | 60 | ## [1.6.1](https://github.com/node-modules/detect-port/compare/v1.6.0...v1.6.1) (2024-05-08) 61 | 62 | 63 | ### Reverts 64 | 65 | * Revert "feat: use address@2 (#53)" (#54) ([48dfe47](https://github.com/node-modules/detect-port/commit/48dfe47d63f137b05f6a540ccfc0f0fa133a847a)), closes [#53](https://github.com/node-modules/detect-port/issues/53) [#54](https://github.com/node-modules/detect-port/issues/54) 66 | 67 | ## [1.6.0](https://github.com/node-modules/detect-port/compare/v1.5.1...v1.6.0) (2024-05-08) 68 | 69 | 70 | ### Features 71 | 72 | * use address@2 ([#53](https://github.com/node-modules/detect-port/issues/53)) ([55f48d7](https://github.com/node-modules/detect-port/commit/55f48d755f3c8b480d4e4ce1065abc1c8e3c5a19)) 73 | 74 | --- 75 | 76 | 77 | 1.5.1 / 2022-09-23 78 | ================== 79 | 80 | **fixes** 81 | * [[`9dd9ce3`](http://github.com/node-modules/detect-port/commit/9dd9ce34b560a434ee3a393f6ddea884691f632f)] - fix: add #!/usr/bin/env node header (#49) (达峰的夏天 <>) 82 | 83 | 1.5.0 / 2022-09-21 84 | ================== 85 | 86 | **features** 87 | * [[`618dec5`](http://github.com/node-modules/detect-port/commit/618dec5661d94535800089f9d941f4896825cb69)] - feat: support wait port (#46) (达峰的夏天 <>) 88 | 89 | **fixes** 90 | * [[`a54e2ef`](http://github.com/node-modules/detect-port/commit/a54e2ef70e388ed4b0c7a4b79ad88bc91e0f8ae3)] - fix: typo on line 54 (#45) (Yavuz Akyuz <<56271907+yavuzakyuz@users.noreply.github.com>>) 91 | 92 | **others** 93 | * [[`28f07b3`](http://github.com/node-modules/detect-port/commit/28f07b31a7c591cb28b13281246c7f0c64c3dded)] - 🤖 TEST: Run CI on Github Action (#47) (fengmk2 <>) 94 | * [[`ae55c95`](http://github.com/node-modules/detect-port/commit/ae55c956ca36749e22c48b8d1a7d98afec2e6a4d)] - Create codeql-analysis.yml (fengmk2 <>) 95 | * [[`f35409d`](http://github.com/node-modules/detect-port/commit/f35409d53f9298a60e2c6c1560f42ea182025dd4)] - chore: update project config (xudafeng <>) 96 | * [[`cd21d30`](http://github.com/node-modules/detect-port/commit/cd21d3044db73d1556bf264209c8fd0ee08fa9c4)] - chore: update readme (#43) (XiaoRui <>) 97 | * [[`da01e68`](http://github.com/node-modules/detect-port/commit/da01e68b43952e06430cc42f873e4253d8cba09e)] - chore: add .editorconfig (#42) (达峰的夏天 <>) 98 | * [[`a2c6b04`](http://github.com/node-modules/detect-port/commit/a2c6b043954895cba9cbae369e0d79a337c9d73a)] - chore: update repo config (#41) (达峰的夏天 <>) 99 | * [[`8da6f33`](http://github.com/node-modules/detect-port/commit/8da6f33e10b44cdbcfb9eb5727b0f2117e6929e9)] - chore: update readme (#38) (达峰的夏天 <>) 100 | * [[`ee88ccb`](http://github.com/node-modules/detect-port/commit/ee88ccb9e2a747dc84a30bcfc1cd4c73b64e3ea5)] - chore: remove unuse file (fengmk2 <>) 101 | 102 | 1.3.0 / 2018-11-20 103 | ================== 104 | 105 | **features** 106 | * [[`a00357a`](http://github.com/node-modules/detect-port/commit/a00357aea32c4f011b7240641cb8da2dfc97b491)] - feat: support detect port with custom hostname (#35) (Ender Lee <<34906299+chnliquan@users.noreply.github.com>>) 107 | 108 | **others** 109 | * [[`671094f`](http://github.com/node-modules/detect-port/commit/671094f3a3660a29a0920d78e39d17f8dead0b7a)] - update readme (xudafeng <>) 110 | * [[`285e59b`](http://github.com/node-modules/detect-port/commit/285e59b0464d670c886007ff5052892393d57314)] - chore: add files to package.json (fengmk2 <>) 111 | 112 | 1.2.3 / 2018-05-16 113 | ================== 114 | 115 | **fixes** 116 | * [[`64777f8`](http://github.com/node-modules/detect-port/commit/64777f85cc519c9c4c2c84c23d2afed6a916f3c4)] - fix: ignore EADDRNOTAVAIL error when listen localhost (#33) (Haoliang Gao <>) 117 | * [[`398bc4f`](http://github.com/node-modules/detect-port/commit/398bc4f65f4d61ddfdc9bf7721118ea1a3bb6289)] - fix: handle 0.0.0.0:port binding (#26) (fengmk2 <>) 118 | 119 | **others** 120 | * [[`aedf44f`](http://github.com/node-modules/detect-port/commit/aedf44fc3f949de9ec187bdc8ee4d8daf84d6c2b)] - doc: tweak description (xudafeng <>) 121 | * [[`b7ff76f`](http://github.com/node-modules/detect-port/commit/b7ff76f24db3d8d9123cbf396b9032b05a6b7146)] - update FAQ & contributor (xudafeng <>) 122 | * [[`4a9e127`](http://github.com/node-modules/detect-port/commit/4a9e127b6d01bd45d9b689bd931d878aa9b5d397)] - cli tweak to verbose (#25) (xdf <>), 123 | 124 | 1.1.3 / 2017-05-24 125 | ================== 126 | 127 | * fix: should ignore getaddrinfo ENOTFOUND error (#22) 128 | 129 | 1.1.2 / 2017-05-11 130 | ================== 131 | 132 | * fix: should double check 0.0.0.0 and localhost (#20) 133 | * docs: ignore type of port when checking if it's occupied (#18) 134 | 135 | # 1.1.1 / 2017-03-17 136 | 137 | * fix: try to use next available port (#16) 138 | 139 | # 1.1.0 / 2016-01-17 140 | 141 | * Use server listen to detect port 142 | 143 | # 1.0.7 / 2016-12-11 144 | 145 | * Early return for rejected promise 146 | * Prevent promsie swallow in callback 147 | 148 | # 1.0.6 / 2016-11-29 149 | 150 | * Bump version for new Repo 151 | 152 | # 0.1.4 / 2015-08-24 153 | 154 | * Support promise 155 | 156 | # 0.1.2 / 2014-05-31 157 | 158 | * Fix commander 159 | 160 | # 0.1.1 / 2014-05-30 161 | 162 | * Add command line support 163 | 164 | # 0.1.0 / 2014-05-29 165 | 166 | * Initial release 167 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to detect-port 2 | 3 | - Fork the project, make a change, and send a pull request; 4 | - Have a look at code style now before starting; 5 | - Make sure the tests case (`$ npm test`) pass before sending a pull request; 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 - present node-modules and other contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # detect-port 2 | 3 | [![NPM version][npm-image]][npm-url] 4 | [![CI](https://github.com/node-modules/detect-port/actions/workflows/nodejs.yml/badge.svg)](https://github.com/node-modules/detect-port/actions/workflows/nodejs.yml) 5 | [![Test coverage][codecov-image]][codecov-url] 6 | [![Known Vulnerabilities][snyk-image]][snyk-url] 7 | [![npm download][download-image]][download-url] 8 | [![Node.js Version][node-version-image]][node-version-url] 9 | 10 | [npm-image]: https://img.shields.io/npm/v/detect-port.svg?style=flat-square 11 | [npm-url]: https://npmjs.org/package/detect-port 12 | [codecov-image]: https://codecov.io/gh/node-modules/detect-port/branch/master/graph/badge.svg 13 | [codecov-url]: https://codecov.io/gh/node-modules/detect-port 14 | [snyk-image]: https://snyk.io/test/npm/detect-port/badge.svg?style=flat-square 15 | [snyk-url]: https://snyk.io/test/npm/detect-port 16 | [download-image]: https://img.shields.io/npm/dm/detect-port.svg?style=flat-square 17 | [download-url]: https://npmjs.org/package/detect-port 18 | [node-version-image]: https://img.shields.io/node/v/detect-port.svg?style=flat-square 19 | [node-version-url]: https://nodejs.org/en/download/ 20 | 21 | > Node.js implementation of port detector 22 | 23 | ## Who are using or has used 24 | 25 | - ⭐⭐⭐[eggjs/egg](//github.com/eggjs/egg) 26 | - ⭐⭐⭐[alibaba/ice](//github.com/alibaba/ice) 27 | - ⭐⭐⭐[alibaba/uirecorder](//github.com/alibaba/uirecorder) 28 | - ⭐⭐⭐[facebook/create-react-app](//github.com/facebook/create-react-app/blob/main/packages/react-dev-utils/package.json) 29 | - ⭐⭐⭐[facebook/flipper](//github.com/facebook/flipper) 30 | - ⭐⭐⭐[umijs/umi](//github.com/umijs/umi) 31 | - ⭐⭐⭐[gatsbyjs/gatsby](//github.com/gatsbyjs/gatsby) 32 | - ⭐⭐⭐[electron-react-boilerplate/electron-react-boilerplate](//github.com/electron-react-boilerplate/electron-react-boilerplate) 33 | - ⭐⭐⭐[zeit/micro](//github.com/zeit/micro) 34 | - ⭐⭐⭐[rails/webpacker](//github.com/rails/webpacker) 35 | - ⭐⭐⭐[storybookjs/storybook](//github.com/storybookjs/storybook) 36 | 37 | [For more](//github.com/node-modules/detect-port/network/dependents) 38 | 39 | ## Usage 40 | 41 | ```bash 42 | npm i detect-port 43 | ``` 44 | 45 | CommonJS 46 | 47 | ```javascript 48 | const { detect } = require('detect-port'); 49 | 50 | detect(port) 51 | .then(realPort => { 52 | if (port == realPort) { 53 | console.log(`port: ${port} was not occupied`); 54 | } else { 55 | console.log(`port: ${port} was occupied, try port: ${realPort}`); 56 | } 57 | }) 58 | .catch(err => { 59 | console.log(err); 60 | }); 61 | ``` 62 | 63 | ESM and TypeScript 64 | 65 | ```ts 66 | import { detect } from 'detect-port'; 67 | 68 | detect(port) 69 | .then(realPort => { 70 | if (port == realPort) { 71 | console.log(`port: ${port} was not occupied`); 72 | } else { 73 | console.log(`port: ${port} was occupied, try port: ${realPort}`); 74 | } 75 | }) 76 | .catch(err => { 77 | console.log(err); 78 | }); 79 | ``` 80 | 81 | ## Command Line Tool 82 | 83 | ```bash 84 | npm i detect-port -g 85 | ``` 86 | 87 | ### Quick Start 88 | 89 | ```bash 90 | # get an available port randomly 91 | $ detect 92 | 93 | # detect pointed port 94 | $ detect 80 95 | 96 | # output verbose log 97 | $ detect --verbose 98 | 99 | # more help 100 | $ detect --help 101 | ``` 102 | 103 | ## FAQ 104 | 105 | Most likely network error, check that your `/etc/hosts` and make sure the content below: 106 | 107 | ```bash 108 | 127.0.0.1 localhost 109 | 255.255.255.255 broadcasthost 110 | ::1 localhost 111 | ``` 112 | 113 | ## License 114 | 115 | [MIT](LICENSE) 116 | 117 | ## Contributors 118 | 119 | [![Contributors](https://contrib.rocks/image?repo=node-modules/detect-port)](https://github.com/node-modules/detect-port/graphs/contributors) 120 | 121 | Made with [contributors-img](https://contrib.rocks). 122 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "detect-port", 3 | "version": "2.1.0", 4 | "description": "Node.js implementation of port detector", 5 | "keywords": [ 6 | "detect", 7 | "port" 8 | ], 9 | "bin": { 10 | "detect": "dist/commonjs/bin/detect-port.js", 11 | "detect-port": "dist/commonjs/bin/detect-port.js" 12 | }, 13 | "main": "./dist/commonjs/index.js", 14 | "files": [ 15 | "dist", 16 | "src" 17 | ], 18 | "repository": { 19 | "type": "git", 20 | "url": "git://github.com/node-modules/detect-port.git" 21 | }, 22 | "dependencies": { 23 | "address": "^2.0.1" 24 | }, 25 | "devDependencies": { 26 | "@eggjs/tsconfig": "^1.3.3", 27 | "@types/mocha": "^10.0.6", 28 | "@types/node": "^22.10.1", 29 | "egg-bin": "^6.9.0", 30 | "eslint": "^8.52.0", 31 | "eslint-config-egg": "^13.0.0", 32 | "execa": "^8.0.1", 33 | "mm": "^3.4.0", 34 | "strip-ansi": "^7.1.0", 35 | "tshy": "^3.0.2", 36 | "tshy-after": "^1.0.0", 37 | "typescript": "^5.2.2" 38 | }, 39 | "scripts": { 40 | "pretest": "npm run lint -- --fix && npm run prepublishOnly", 41 | "test": "egg-bin test", 42 | "lint": "eslint src test --ext ts", 43 | "ci": "npm run lint && npm run cov && npm run prepublishOnly", 44 | "prepublishOnly": "tshy && tshy-after", 45 | "precov": "npm run prepublishOnly", 46 | "cov": "egg-bin cov" 47 | }, 48 | "engines": { 49 | "node": ">= 16.0.0" 50 | }, 51 | "homepage": "https://github.com/node-modules/detect-port", 52 | "license": "MIT", 53 | "tshy": { 54 | "exports": { 55 | ".": "./src/index.ts" 56 | } 57 | }, 58 | "exports": { 59 | ".": { 60 | "import": { 61 | "types": "./dist/esm/index.d.ts", 62 | "default": "./dist/esm/index.js" 63 | }, 64 | "require": { 65 | "types": "./dist/commonjs/index.d.ts", 66 | "default": "./dist/commonjs/index.js" 67 | } 68 | } 69 | }, 70 | "types": "./dist/commonjs/index.d.ts", 71 | "type": "module", 72 | "module": "./dist/esm/index.js" 73 | } 74 | -------------------------------------------------------------------------------- /src/bin/detect-port.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import path from 'node:path'; 4 | import { readFileSync } from 'node:fs'; 5 | import { detectPort } from '../detect-port.js'; 6 | 7 | const pkgFile = path.join(__dirname, '../../../package.json'); 8 | const pkg = JSON.parse(readFileSync(pkgFile, 'utf-8')); 9 | 10 | const args = process.argv.slice(2); 11 | let arg_0 = args[0]; 12 | 13 | if (arg_0 && [ '-v', '--version' ].includes(arg_0.toLowerCase())) { 14 | console.log(pkg.version); 15 | process.exit(0); 16 | } 17 | 18 | const removeByValue = (arr: string[], val: string) => { 19 | for (let i = 0; i < arr.length; i++) { 20 | if (arr[i] === val) { 21 | arr.splice(i, 1); 22 | break; 23 | } 24 | } 25 | }; 26 | 27 | const port = parseInt(arg_0, 10); 28 | const isVerbose = args.includes('--verbose'); 29 | 30 | removeByValue(args, '--verbose'); 31 | arg_0 = args[0]; 32 | if (!arg_0) { 33 | const random = Math.floor(9000 + Math.random() * (65535 - 9000)); 34 | 35 | detectPort(random, (err, port) => { 36 | if (isVerbose) { 37 | if (err) { 38 | console.log(`get available port failed with ${err}`); 39 | } 40 | console.log(`get available port ${port} randomly`); 41 | } else { 42 | console.log(port || random); 43 | } 44 | }); 45 | } else if (isNaN(port)) { 46 | console.log(); 47 | console.log(` \u001b[37m${pkg.description}\u001b[0m`); 48 | console.log(); 49 | console.log(' Usage:'); 50 | console.log(); 51 | console.log(` ${pkg.name} [port]`); 52 | console.log(); 53 | console.log(' Options:'); 54 | console.log(); 55 | console.log(' -v, --version output version and exit'); 56 | console.log(' -h, --help output usage information'); 57 | console.log(' --verbose output verbose log'); 58 | console.log(); 59 | console.log(' Further help:'); 60 | console.log(); 61 | console.log(` ${pkg.homepage}`); 62 | console.log(); 63 | } else { 64 | detectPort(port, (err, _port) => { 65 | if (isVerbose) { 66 | if (err) { 67 | console.log(`get available port failed with ${err}`); 68 | } 69 | 70 | if (port !== _port) { 71 | console.log(`port ${port} was occupied`); 72 | } 73 | 74 | console.log(`get available port ${_port}`); 75 | } else { 76 | console.log(_port || port); 77 | } 78 | }); 79 | } 80 | -------------------------------------------------------------------------------- /src/detect-port.ts: -------------------------------------------------------------------------------- 1 | import { createServer, AddressInfo } from 'node:net'; 2 | import { debuglog } from 'node:util'; 3 | import { ip } from 'address'; 4 | 5 | const debug = debuglog('detect-port'); 6 | 7 | export type DetectPortCallback = (err: Error | null, port?: number) => void; 8 | 9 | export interface PortConfig { 10 | port?: number | string; 11 | hostname?: string | undefined; 12 | callback?: DetectPortCallback; 13 | } 14 | 15 | export class IPAddressNotAvailableError extends Error { 16 | constructor(options?: ErrorOptions) { 17 | super('The IP address is not available on this machine', options); 18 | this.name = this.constructor.name; 19 | Error.captureStackTrace(this, this.constructor); 20 | } 21 | } 22 | 23 | export function detectPort(port?: number | PortConfig | string): Promise; 24 | export function detectPort(callback: DetectPortCallback): void; 25 | export function detectPort(port: number | PortConfig | string | undefined, callback: DetectPortCallback): void; 26 | export function detectPort(port?: number | string | PortConfig | DetectPortCallback, callback?: DetectPortCallback) { 27 | let hostname: string | undefined = ''; 28 | 29 | if (port && typeof port === 'object') { 30 | hostname = port.hostname; 31 | callback = port.callback; 32 | port = port.port; 33 | } else { 34 | if (typeof port === 'function') { 35 | callback = port; 36 | port = void 0; 37 | } 38 | } 39 | 40 | port = parseInt(port as unknown as string) || 0; 41 | let maxPort = port + 10; 42 | if (maxPort > 65535) { 43 | maxPort = 65535; 44 | } 45 | debug('detect free port between [%s, %s)', port, maxPort); 46 | if (typeof callback === 'function') { 47 | return tryListen(port, maxPort, hostname) 48 | .then(port => callback(null, port)) 49 | .catch(callback); 50 | } 51 | // promise 52 | return tryListen(port as number, maxPort, hostname); 53 | } 54 | 55 | async function handleError(port: number, maxPort: number, hostname?: string) { 56 | if (port >= maxPort) { 57 | debug('port: %s >= maxPort: %s, give up and use random port', port, maxPort); 58 | port = 0; 59 | maxPort = 0; 60 | } 61 | return await tryListen(port, maxPort, hostname); 62 | } 63 | 64 | async function tryListen(port: number, maxPort: number, hostname?: string): Promise { 65 | // use user hostname 66 | if (hostname) { 67 | try { 68 | return await listen(port, hostname); 69 | } catch (err: any) { 70 | if (err.code === 'EADDRNOTAVAIL') { 71 | throw new IPAddressNotAvailableError({ cause: err }); 72 | } 73 | return await handleError(++port, maxPort, hostname); 74 | } 75 | } 76 | 77 | // 1. check null / undefined 78 | try { 79 | await listen(port); 80 | } catch (err) { 81 | // ignore random listening 82 | if (port === 0) { 83 | throw err; 84 | } 85 | return await handleError(++port, maxPort, hostname); 86 | } 87 | 88 | // 2. check 0.0.0.0 89 | try { 90 | await listen(port, '0.0.0.0'); 91 | } catch (err) { 92 | return await handleError(++port, maxPort, hostname); 93 | } 94 | 95 | // 3. check 127.0.0.1 96 | try { 97 | await listen(port, '127.0.0.1'); 98 | } catch (err) { 99 | return await handleError(++port, maxPort, hostname); 100 | } 101 | 102 | // 4. check localhost 103 | try { 104 | await listen(port, 'localhost'); 105 | } catch (err: any) { 106 | // if localhost refer to the ip that is not unknown on the machine, you will see the error EADDRNOTAVAIL 107 | // https://stackoverflow.com/questions/10809740/listen-eaddrnotavail-error-in-node-js 108 | if (err.code !== 'EADDRNOTAVAIL') { 109 | return await handleError(++port, maxPort, hostname); 110 | } 111 | } 112 | 113 | // 5. check current ip 114 | try { 115 | return await listen(port, ip()); 116 | } catch (err) { 117 | return await handleError(++port, maxPort, hostname); 118 | } 119 | } 120 | 121 | function listen(port: number, hostname?: string) { 122 | const server = createServer(); 123 | 124 | return new Promise((resolve, reject) => { 125 | server.once('error', err => { 126 | debug('listen %s:%s error: %s', hostname, port, err); 127 | server.close(); 128 | 129 | if ((err as any).code === 'ENOTFOUND') { 130 | debug('ignore dns ENOTFOUND error, get free %s:%s', hostname, port); 131 | return resolve(port); 132 | } 133 | 134 | return reject(err); 135 | }); 136 | 137 | debug('try listen %d on %s', port, hostname); 138 | server.listen(port, hostname, () => { 139 | port = (server.address() as AddressInfo).port; 140 | debug('get free %s:%s', hostname, port); 141 | server.close(); 142 | return resolve(port); 143 | }); 144 | }); 145 | } 146 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { detectPort } from './detect-port.js'; 2 | 3 | export default detectPort; 4 | 5 | export * from './detect-port.js'; 6 | // keep alias detectPort to detect 7 | export const detect = detectPort; 8 | 9 | export * from './wait-port.js'; 10 | -------------------------------------------------------------------------------- /src/wait-port.ts: -------------------------------------------------------------------------------- 1 | import { debuglog } from 'node:util'; 2 | import { detectPort } from './detect-port.js'; 3 | 4 | const debug = debuglog('detect-port:wait-port'); 5 | 6 | function sleep(ms: number) { 7 | return new Promise(resolve => { 8 | setTimeout(resolve, ms); 9 | }); 10 | } 11 | 12 | export class WaitPortRetryError extends Error { 13 | retries: number; 14 | count: number; 15 | 16 | constructor(message: string, retries: number, count: number, options?: ErrorOptions) { 17 | super(message, options); 18 | this.name = this.constructor.name; 19 | this.retries = retries; 20 | this.count = count; 21 | Error.captureStackTrace(this, this.constructor); 22 | } 23 | } 24 | 25 | export interface WaitPortOptions { 26 | retryInterval?: number; 27 | retries?: number; 28 | } 29 | 30 | export async function waitPort(port: number, options: WaitPortOptions = {}) { 31 | const { retryInterval = 1000, retries = Infinity } = options; 32 | let count = 1; 33 | 34 | async function loop() { 35 | debug('wait port %d, retries %d, count %d', port, retries, count); 36 | if (count > retries) { 37 | const err = new WaitPortRetryError('retries exceeded', retries, count); 38 | throw err; 39 | } 40 | count++; 41 | const freePort = await detectPort(port); 42 | if (freePort === port) { 43 | await sleep(retryInterval); 44 | return loop(); 45 | } 46 | return true; 47 | } 48 | 49 | return await loop(); 50 | } 51 | -------------------------------------------------------------------------------- /test/cli.test.ts: -------------------------------------------------------------------------------- 1 | import stripAnsi from 'strip-ansi'; 2 | import path from 'node:path'; 3 | import { fileURLToPath } from 'node:url'; 4 | import { execaNode } from 'execa'; 5 | import { strict as assert } from 'node:assert'; 6 | import { readFileSync } from 'node:fs'; 7 | 8 | const __filename = fileURLToPath(import.meta.url); 9 | const __dirname = path.dirname(__filename); 10 | const pkgFile = path.join(__dirname, '../package.json'); 11 | const pkg = JSON.parse(readFileSync(pkgFile, 'utf-8')); 12 | 13 | describe('test/cli.test.ts', async () => { 14 | const binFile = path.join(__dirname, '../dist/commonjs/bin/detect-port.js'); 15 | 16 | it('should show version', async () => { 17 | let res = await execaNode(binFile, [ '-v' ]); 18 | assert(res.stdout, pkg.version); 19 | res = await execaNode(binFile, [ '--version' ]); 20 | assert(res.stdout, pkg.version); 21 | }); 22 | 23 | it('should output usage information', async () => { 24 | let res = await execaNode(binFile, [ '-h' ]); 25 | assert(res.stdout.includes(pkg.description)); 26 | res = await execaNode(binFile, [ '--help' ]); 27 | assert(res.stdout.includes(pkg.description)); 28 | res = await execaNode(binFile, [ 'help' ]); 29 | assert(res.stdout.includes(pkg.description)); 30 | res = await execaNode(binFile, [ 'xxx' ]); 31 | assert(res.stdout.includes(pkg.description)); 32 | }); 33 | 34 | // it('should output available port randomly', { only: true }, async () => { 35 | // const res = await execaNode(binFile); 36 | // const port = parseInt(stripAnsi(res.stdout).trim(), 10); 37 | // assert(port >= 9000 && port < 65535); 38 | // }); 39 | 40 | it('should output available port from the given port', async () => { 41 | const givenPort = 9000; 42 | const res = await execaNode(binFile, [ givenPort + '' ]); 43 | const port = parseInt(stripAnsi(res.stdout).trim(), 10); 44 | assert(port >= givenPort && port < 65535); 45 | }); 46 | 47 | it('should output verbose logs', async () => { 48 | const res = await execaNode(binFile, [ '--verbose' ]); 49 | assert(res.stdout.includes('random')); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /test/demo/detect.js: -------------------------------------------------------------------------------- 1 | import { detect } from '../../dist/esm/index.js'; 2 | 3 | detect(7001) 4 | .then(port => { 5 | console.log(port); 6 | }) 7 | .catch(err => { 8 | console.error(err); 9 | }); 10 | -------------------------------------------------------------------------------- /test/demo/server.js: -------------------------------------------------------------------------------- 1 | import { createServer } from 'node:http'; 2 | import { hostname } from 'node:os'; 3 | 4 | const server = createServer(); 5 | 6 | server.listen(7001, hostname(), () => { 7 | console.log('listening %s:7001, address: %o', hostname(), server.address()); 8 | }); 9 | -------------------------------------------------------------------------------- /test/detect-port.test.ts: -------------------------------------------------------------------------------- 1 | import dns from 'node:dns'; 2 | import net from 'node:net'; 3 | import { strict as assert } from 'node:assert'; 4 | import { ip } from 'address'; 5 | import mm from 'mm'; 6 | import detect from '../src/index.js'; 7 | import { detect as detect2, detectPort } from '../src/index.js'; 8 | 9 | describe('test/detect-port.test.ts', () => { 10 | afterEach(mm.restore); 11 | 12 | describe('detect port test', () => { 13 | const servers: net.Server[] = []; 14 | before(done => { 15 | let count = 0; 16 | const cb = (err?: Error) => { 17 | if (err) { 18 | done(err); 19 | } 20 | count += 1; 21 | if (count === 13) { 22 | done(); 23 | } 24 | }; 25 | const server = new net.Server(); 26 | server.listen(23000, 'localhost', cb); 27 | server.on('error', err => { 28 | console.error('listen localhost error:', err); 29 | }); 30 | servers.push(server); 31 | 32 | const server2 = new net.Server(); 33 | server2.listen(24000, ip(), cb); 34 | servers.push(server2); 35 | 36 | const server3 = new net.Server(); 37 | server3.listen(28080, '0.0.0.0', cb); 38 | servers.push(server3); 39 | 40 | const server4 = new net.Server(); 41 | server4.listen(25000, '127.0.0.1', cb); 42 | server4.on('error', err => { 43 | console.error('listen 127.0.0.1 error:', err); 44 | }); 45 | servers.push(server4); 46 | 47 | const server5 = new net.Server(); 48 | server5.listen(25500, '::1', cb); 49 | server5.on('error', err => { 50 | console.error('listen ::1 error:', err); 51 | }); 52 | servers.push(server5); 53 | 54 | for (let port = 27000; port < 27010; port++) { 55 | const server = new net.Server(); 56 | if (port % 3 === 0) { 57 | server.listen(port, cb); 58 | } else if (port % 3 === 1) { 59 | server.listen(port, 'localhost', cb); 60 | } else { 61 | server.listen(port, ip(), cb); 62 | } 63 | servers.push(server); 64 | } 65 | }); 66 | 67 | after(() => { 68 | servers.forEach(server => server.close()); 69 | }); 70 | 71 | it('get random port with callback', done => { 72 | detectPort((_, port) => { 73 | assert(port); 74 | assert(port >= 1024 && port < 65535); 75 | done(); 76 | }); 77 | }); 78 | 79 | it('get random port with promise', async () => { 80 | const port = await detectPort(); 81 | 82 | assert(port >= 1024 && port < 65535); 83 | }); 84 | 85 | it('should detect work', async () => { 86 | let port = await detect(); 87 | assert(port >= 1024 && port < 65535); 88 | port = await detect2(); 89 | assert(port >= 1024 && port < 65535); 90 | }); 91 | 92 | it('with occupied port, like "listen EACCES: permission denied"', async () => { 93 | const port = 80; 94 | const realPort = await detectPort(port); 95 | assert(realPort >= port && realPort < 65535); 96 | }); 97 | 98 | it('work with listening next port 23001 because 23000 was listened to localhost', async () => { 99 | const port = 23000; 100 | const realPort = await detectPort(port); 101 | assert(realPort); 102 | assert.equal(realPort, 23001); 103 | }); 104 | 105 | it('work with listening next port 25001 because 25000 was listened to 127.0.0.1', async () => { 106 | const port = 25000; 107 | const realPort = await detectPort(port); 108 | assert(realPort); 109 | assert.equal(realPort, 25001); 110 | }); 111 | 112 | it('work with listening next port 25501 because 25500 was listened to ::1', async () => { 113 | const port = 25500; 114 | const realPort = await detectPort(port); 115 | assert(realPort); 116 | assert.equal(realPort, 25501); 117 | }); 118 | 119 | it('should listen next port 24001 when localhost is not binding', async () => { 120 | mm(dns, 'lookup', (...args: any[]) => { 121 | mm.restore(); 122 | const address = args[0] as string; 123 | if (address !== 'localhost') { 124 | return dns.lookup(args[0], args[1], args[2]); 125 | } 126 | process.nextTick(() => { 127 | const err = new Error(`getaddrinfo ENOTFOUND ${address}`); 128 | (err as any).code = 'ENOTFOUND'; 129 | const callback = args[-1]; 130 | callback(err); 131 | }); 132 | }); 133 | 134 | const port = 24000; 135 | const realPort = await detectPort(port); 136 | assert.equal(realPort, 24001); 137 | }); 138 | 139 | it('work with listening next port 24001 because 24000 was listened', async () => { 140 | const port = 24000; 141 | const realPort = await detectPort(port); 142 | assert.equal(realPort, 24001); 143 | }); 144 | 145 | it('work with listening next port 28081 because 28080 was listened to 0.0.0.0:28080', async () => { 146 | const port = 28080; 147 | const realPort = await detectPort(port); 148 | 149 | assert.equal(realPort, 28081); 150 | }); 151 | 152 | it('work with listening random port when try port hit maxPort', async () => { 153 | const port = 27000; 154 | const realPort = await detectPort(port); 155 | assert(realPort < 27000 || realPort > 27009); 156 | }); 157 | 158 | it('work with sending object with hostname', done => { 159 | const port = 27000; 160 | const hostname = '127.0.0.1'; 161 | detectPort({ 162 | port, 163 | hostname, 164 | callback: (_, realPort) => { 165 | assert(realPort); 166 | assert(realPort >= 27000 && realPort < 65535); 167 | done(); 168 | }, 169 | }); 170 | }); 171 | 172 | it('promise with sending object with hostname', async () => { 173 | const port = 27000; 174 | const hostname = '127.0.0.1'; 175 | const realPort = await detectPort({ 176 | port, 177 | hostname, 178 | }); 179 | assert(realPort >= 27000 && realPort < 65535); 180 | }); 181 | 182 | it('with string arg', async () => { 183 | const port = '28080'; 184 | const realPort = await detectPort(port); 185 | assert(realPort >= 28080 && realPort < 65535); 186 | }); 187 | 188 | it('with wrong arguments', async () => { 189 | const port = await detectPort('oooo'); 190 | assert(port && port > 0); 191 | }); 192 | 193 | it('async/await usage', async () => { 194 | const port = 28080; 195 | const realPort = await detectPort(port); 196 | assert(realPort >= port && realPort < 65535); 197 | }); 198 | 199 | it('promise usage', done => { 200 | const _port = 28080; 201 | detectPort(_port) 202 | .then(port => { 203 | assert(port >= _port && port < 65535); 204 | done(); 205 | }) 206 | .catch(done); 207 | }); 208 | 209 | it('promise with wrong arguments', done => { 210 | detectPort() 211 | .then(port => { 212 | assert(port > 0); 213 | done(); 214 | }) 215 | .catch(done); 216 | }); 217 | 218 | it('generator with wrong arguments and return random port', async () => { 219 | const port = await detectPort('oooo'); 220 | assert(port > 0); 221 | assert(typeof port === 'number'); 222 | }); 223 | }); 224 | }); 225 | -------------------------------------------------------------------------------- /test/wait-port.test.ts: -------------------------------------------------------------------------------- 1 | import { once } from 'node:events'; 2 | import { createServer, Server } from 'node:net'; 3 | import { strict as assert } from 'node:assert'; 4 | import { waitPort, detectPort, WaitPortRetryError } from '../src/index.js'; 5 | 6 | describe('test/wait-port.test.ts', () => { 7 | describe('wait for port', () => { 8 | const servers: Server[] = []; 9 | after(() => { 10 | servers.forEach(server => server.close()); 11 | }); 12 | 13 | it('should be work', async () => { 14 | const port = await detectPort(); 15 | const server = createServer(); 16 | servers.push(server); 17 | server.listen(port, '0.0.0.0'); 18 | await once(server, 'listening'); 19 | setTimeout(() => { 20 | server.close(); 21 | }, 2000); 22 | await waitPort(port); 23 | }); 24 | 25 | it('should be work when retries exceeded', async () => { 26 | try { 27 | const port = 9093; 28 | await waitPort(port, { retries: 3, retryInterval: 100 }); 29 | } catch (err: unknown) { 30 | assert(err instanceof WaitPortRetryError); 31 | assert.equal(err.message, 'retries exceeded'); 32 | assert.equal(err.retries, 3); 33 | assert.equal(err.count, 4); 34 | } 35 | }); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@eggjs/tsconfig", 3 | "compilerOptions": { 4 | "strict": true, 5 | "noImplicitAny": true, 6 | "target": "ES2022", 7 | "module": "NodeNext", 8 | "moduleResolution": "NodeNext" 9 | } 10 | } 11 | --------------------------------------------------------------------------------