dev-server-ports
Version:
Gracefully pick a port number for the dev server
551 lines (482 loc) • 19.7 kB
JavaScript
;
Object.defineProperty(exports, '__esModule', { value: true });
function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }
var child_process = _interopDefault(require('child_process'));
var path = _interopDefault(require('path'));
var readline = _interopDefault(require('readline'));
var detect = _interopDefault(require('detect-port'));
var inquirer = _interopDefault(require('inquirer'));
var isRoot = _interopDefault(require('is-root'));
var tslib = require('tslib');
var chalk = _interopDefault(require('chalk'));
/**
* MIT License
*
* Copyright (c) 2022 Brion Mario
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
// Reserved well-known ports that require sudo permissions.
var WELL_KNOWN_PORT_RANGE = [0, 1024];
/**
* MIT License
*
* Copyright (c) 2022 Brion Mario
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
var Reporter = /*#__PURE__*/function () {
/**
* Constructor.
* @param extensions - Extensions.
* @param overrides - Overrides.
*/
function Reporter(extensions, overrides) {
Reporter.extensions = extensions;
Reporter.overrides = overrides;
}
/**
* Get the root permission required message
*
* @param wellknownPortRange - Range of reserved wellknown ports.
* @returns Returns a formatted root permission required message as a string.
*/
var _proto = Reporter.prototype;
_proto.getMissingRootPermissionMessage = function getMissingRootPermissionMessage(wellknownPortRange) {
return chalk.redBright("Admin permissions are required to run a server on a port below " + wellknownPortRange[1] + ".");
}
/**
* Get the process information report.
*
* @param process - Process info.
* @returns Returns a formatted process info report as a string.
*/
;
_proto.getProcessInfoReport = function getProcessInfoReport(process) {
return "\n process : " + chalk.cyan(process.command) + "\n pid : " + chalk.grey(process.pid) + "\n invoked from : " + chalk.blue(process.directory);
}
/**
* Get the port in use disclaimer message.
*
* @param port - Port which the server is running on.
* @returns Returns a formatted port in use disclaimer message as a string.
*/
;
_proto.getPortInUseDisclaimerMessage = function getPortInUseDisclaimerMessage(port) {
return chalk.bgRedBright(chalk.whiteBright("PORT IN USE")) + " " + chalk.white("-") + " Someone is already using the port " + chalk.bold(chalk.yellowBright(port)) + ".";
}
/**
* Get the port fallback confirmarion message.
*
* @param availablePorts - Available ports.
* @returns Returns a formatted port fallback prompt message as a string.
*/
;
_proto.getNonePortFallbackMessage = function getNonePortFallbackMessage(availablePorts) {
return "\n" + chalk.yellowBright("If possible, free up the port or choose an avaiable one.") + "\n\nThe following " + (availablePorts.length > 1 ? "ports are" : "port is") + " available:\n\n " + chalk.green(availablePorts.join("\n")) + "\n\n" + this.getProcessTerminationMessage();
}
/**
* Get the port fallback confirmation message.
*
* @returns Returns a formatted port fallback confirmation message as a string.
*/
;
_proto.getPortFallbackConfirmation = function getPortFallbackConfirmation() {
return "Would you like to run the app on another port instead?";
}
/**
* Build and print the port fallback confirmarion message.
*
* @param port - Requested Port.
* @param availablePort - Aavailable port.
* @param isInteractive - Should show port fallback confirmation on the prompt.
* @returns Returns a formatted port in use prompt as a string.
*/
;
_proto.buildPortInUsePromptMessage = function buildPortInUsePromptMessage(port, availablePort, isInteractive) {
var _getProcessForPort = getProcessForPort(port),
command = _getProcessForPort.command,
directory = _getProcessForPort.directory,
pid = _getProcessForPort.pid;
var confirmation = isInteractive ? this.getPortFallbackConfirmation() : this.getNonePortFallbackMessage([availablePort]);
return this.getPortInUseDisclaimerMessage(port) + "\n " + this.getProcessInfoReport({
command: command,
directory: directory,
pid: pid
}) + "\n \n" + confirmation;
}
/**
* Get the un-interative terminal error message.
*
* @returns Returns un-interative terminal error message as a string.
*/
;
_proto.getUnInteractiveTerminalError = function getUnInteractiveTerminalError() {
return chalk.red("Prompt couldn't be rendered in the current environment.");
}
/**
* Get the generic prompt error message.
*
* @returns Returns a generic prompt error message as a string.
*/
;
_proto.getGenericPromptError = function getGenericPromptError() {
return chalk.red("Something wen wrong when trying to render the prompt.");
}
/**
* Get no open port on host error message.
*
* @param hostname - Host.
* @param error - Error object.
* @returns Returns no open port on host error message as string.
*/
;
_proto.getOpenPortUnAvailablityOnHost = function getOpenPortUnAvailablityOnHost(hostname, error) {
return chalk.red("Could not find an open port at " + chalk.bold(hostname) + ".") + "\n\n (Network error message: " + (error.message || error) + ")\n }";
}
/**
* Get process termination message.
*
* @returns Returns a message containing process termination info.
*/
;
_proto.getProcessTerminationMessage = function getProcessTerminationMessage() {
return "" + chalk.white("Press ctrl/cmd + c to exit.");
};
return Reporter;
}();
tslib.__decorate([override(), extend()], Reporter.prototype, "getMissingRootPermissionMessage", null);
tslib.__decorate([override(), extend()], Reporter.prototype, "getProcessInfoReport", null);
tslib.__decorate([override(), extend()], Reporter.prototype, "getPortInUseDisclaimerMessage", null);
tslib.__decorate([override(), extend()], Reporter.prototype, "getNonePortFallbackMessage", null);
tslib.__decorate([override(), extend()], Reporter.prototype, "getPortFallbackConfirmation", null);
tslib.__decorate([override(), extend()], Reporter.prototype, "buildPortInUsePromptMessage", null);
tslib.__decorate([override(), extend()], Reporter.prototype, "getUnInteractiveTerminalError", null);
tslib.__decorate([override(), extend()], Reporter.prototype, "getGenericPromptError", null);
tslib.__decorate([override(), extend()], Reporter.prototype, "getOpenPortUnAvailablityOnHost", null);
tslib.__decorate([override(), extend()], Reporter.prototype, "getProcessTerminationMessage", null);
/**
* Override decorator.
*
* @returns Returns the result of the execution.
*/
function override() {
return function (_target, propertyKey, descriptor) {
var originalMethod = descriptor.value;
descriptor.value = function () {
var _this$constructor;
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
var result = originalMethod.apply(this, args);
if ((_this$constructor = this.constructor) != null && _this$constructor.overrides && Object.prototype.hasOwnProperty.call(this.constructor.overrides, propertyKey)) {
result = this.constructor.overrides[propertyKey].apply(this, args);
}
return result;
};
};
}
/**
* Extension decorator.
*
* @returns Returns the result of the execution.
*/
function extend() {
return function (_target, propertyKey, descriptor) {
var originalMethod = descriptor.value;
descriptor.value = function () {
var _this$constructor2;
for (var _len2 = arguments.length, args = new Array(_len2), _key2 = 0; _key2 < _len2; _key2++) {
args[_key2] = arguments[_key2];
}
var result = originalMethod.apply(this, args);
if ((_this$constructor2 = this.constructor) != null && _this$constructor2.extensions) {
if (Object.prototype.hasOwnProperty.call(this.constructor.extensions, "BEFORE_" + propertyKey)) {
result = this.constructor.extensions["BEFORE_" + propertyKey].apply(this, args) + "\n\n" + result;
}
if (Object.prototype.hasOwnProperty.call(this.constructor.extensions, "AFTER_" + propertyKey)) {
result = result + "\n\n" + this.constructor.extensions["AFTER_" + propertyKey].apply(this, args);
}
}
return result;
};
};
}
/**
* MIT License
*
* Copyright (c) 2022 Brion Mario
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
/* eslint-disable no-console */
var logger = {
debug: console.debug,
error: console.error,
info: console.log
};
/* eslint-disable no-console */
/**
* MIT License
*
* Copyright (c) 2022 Brion Mario
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
var IS_INTERACTIVE = process.stdout.isTTY;
var execOptions = {
encoding: "utf8",
// eslint-disable-next-line @typescript-eslint/no-array-constructor
stdio: ["pipe", "pipe", "ignore" //stderr
]
};
/**
* Clears the terminal screen.
*
* @example
* Usage:
* ```
* // Clears the terminal.
* clearTerminal();
* ```
*
* @returns Returns a void.
*/
var clearTerminal = function clearTerminal() {
process.stdout.write(process.platform === "win32" ? "\x1B[2J\x1B[0f" : "\x1B[2J\x1B[3J\x1B[H");
};
/**
* Get the process id (pid) on the given port.
*
* @example
* Usage:
* ```
* // Returns a numeric value as a string like "55543".
* getProcessIdOnPort(3000);
* ```
*
* @param port - Port number.
* @returns
*/
var getProcessIdOnPort = function getProcessIdOnPort(port) {
return child_process.execFileSync("lsof", ["-i:" + port, "-P", "-t", "-sTCP:LISTEN"], execOptions).split("\n")[0].trim();
};
/**
* Get the directory of the process with the given id (pid).
*
* @param processId - Process id.
* @returns Returns the directory of the process.
*/
var getDirectoryOfProcessById = function getDirectoryOfProcessById(processId) {
return child_process.execSync("lsof -p " + processId + " | awk '$4==\"cwd\" {for (i=9; i<=NF; i++) printf \"%s \", $i}'", execOptions).trim();
};
/**
* Checks if the process is a known Node.js process.
*
* @param processCommand - Command ran for the process.
* @returns Returns true if the process is a React app.
*/
var isKnownNodeProcess = function isKnownNodeProcess(processCommand) {
return /^node .*react-scripts\/scripts\/start\.js\s?$/.test(processCommand);
};
/**
* Get the package name in the given directory.
*
* @param directory - Directory to search for package.json.
* @returns Returns the package name.
*/
var getPackageNameInDirectory = function getPackageNameInDirectory(directory) {
var packagePath = path.join(directory.trim(), "package.json");
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
return require(packagePath).name;
} catch (e) {
return null;
}
};
/**
* Get the command ran for the process with the given id (pid).
*
* @param processId - Process id.
* @param processDirectory - Directory of the process.
* @returns Returns the command ran for the process.
*/
var getProcessCommand = function getProcessCommand(processId, processDirectory) {
var command = child_process.execSync("ps -o command -p " + processId + " | sed -n 2p", execOptions);
command = command.replace(/\n$/, ""); // If thr process is a known Node.js process, get the package name from `package.json`.
if (isKnownNodeProcess(command)) {
var packageName = getPackageNameInDirectory(processDirectory);
return packageName ? packageName : command;
} else {
return command;
}
};
/**
* Get the process information for the given port.
*
* @param port - Port number.
* @returns Returns the process information or null.
*/
var getProcessForPort = function getProcessForPort(port) {
try {
var pid = getProcessIdOnPort(port);
var directory = getDirectoryOfProcessById(pid);
var command = getProcessCommand(pid, directory);
return {
command: command,
directory: directory,
pid: pid
};
} catch (e) {
return {
command: undefined,
directory: undefined,
pid: undefined
};
}
};
/**
* Shows a message without any interactions and exits the process on `ctrl+c`.
* @param message - Message to show.
*/
var showNonInteractivePrompt = function showNonInteractivePrompt(message) {
logger.info(message);
readline.emitKeypressEvents(process.stdin);
process.stdin.setRawMode(true);
process.stdin.resume();
process.stdin.on("keypress", function (str) {
if (str === "\x03") {
process.exit();
}
});
};
/**
* Find a port that is available for use.
*
* @param port - Preffered port number. ex: 3000
* @param hostname - Host name. ex: "0.0.0.0" | "localhost"
* @param reporter - Reporter overrides and extensions.
* @returns Returns a promise that resolves to the available port or null on error.
*/
var findPort = function findPort(port, hostname, shouldFallback, reporter) {
var _port = typeof port === "number" ? port : parseInt(port, 10);
var _reporter = new Reporter(reporter == null ? void 0 : reporter.extensions, reporter == null ? void 0 : reporter.overrides);
return detect({
hostname: hostname,
port: _port
}).then(function (availablePort) {
return new Promise(function (resolve) {
if (availablePort === _port) {
return resolve(availablePort);
}
var needSudoPermissions = process.platform !== "win32" && (_port < WELL_KNOWN_PORT_RANGE[1] || availablePort < WELL_KNOWN_PORT_RANGE[1]) && !isRoot();
var question = {
"default": true,
message: _reporter.buildPortInUsePromptMessage(_port, availablePort, shouldFallback),
name: "shouldChangePort",
type: "confirm"
}; // If the port needs permission to run, show a message & terminate on ctrl + c.
if (needSudoPermissions) {
showNonInteractivePrompt(_reporter.getMissingRootPermissionMessage(WELL_KNOWN_PORT_RANGE));
return;
}
if (IS_INTERACTIVE) {
// First clear the terminal.
clearTerminal();
if (shouldFallback === false) {
showNonInteractivePrompt(question.message);
} else {
inquirer.prompt([question]).then(function (answers) {
if (answers.shouldChangePort) {
resolve(availablePort);
} else {
process.exit();
}
})["catch"](function (error) {
if (error.isTtyError) {
_reporter.getUnInteractiveTerminalError();
} else {
_reporter.getGenericPromptError();
}
});
}
} else {
showNonInteractivePrompt(question.message);
}
});
}, function (err) {
throw new Error(_reporter.getOpenPortUnAvailablityOnHost(hostname, err));
});
};
exports.clearTerminal = clearTerminal;
exports.findPort = findPort;
exports.getDirectoryOfProcessById = getDirectoryOfProcessById;
exports.getPackageNameInDirectory = getPackageNameInDirectory;
exports.getProcessCommand = getProcessCommand;
exports.getProcessForPort = getProcessForPort;
exports.getProcessIdOnPort = getProcessIdOnPort;
//# sourceMappingURL=dev-server-ports.cjs.development.js.map