UNPKG

start-server-and-test

Version:

Starts server, waits for URL, then runs test command; when the tests end, shuts down server

305 lines (266 loc) 7.36 kB
const { lazyAss: la } = require('lazy-ass') const is = require('check-more-types') const { join } = require('path') const { existsSync } = require('fs') const arg = require('arg') const debug = require('debug')('start-server-and-test') const namedArguments = { '--expect': Number, '--expected': '--expect', '--proxy-host': String, '--proxy-protocol': String, '--proxy-port': Number, '--proxy-user': String, '--proxy-password': String, '--proxy-pass': '--proxy-password', } /** * Returns new array of command line arguments * where leading and trailing " and ' are indicating * the beginning and end of an argument. */ const crossArguments = (cliArguments) => { const args = arg(namedArguments, { permissive: true, argv: cliArguments, }) debug('initial parsed arguments %o', args) // all other arguments const cliArgs = args._ let concatModeChar = false const indicationChars = ["'", '"', '`'] const combinedArgs = [] for (let i = 0; i < cliArgs.length; i++) { let arg = cliArgs[i] if ( !concatModeChar && indicationChars.some((char) => cliArgs[i].startsWith(char)) ) { arg = arg.slice(1) } if (concatModeChar && cliArgs[i].endsWith(concatModeChar)) { arg = arg.slice(0, -1) } if (concatModeChar && combinedArgs.length) { combinedArgs[combinedArgs.length - 1] += ' ' + arg } else { combinedArgs.push(arg) } if ( !concatModeChar && indicationChars.some((char) => cliArgs[i].startsWith(char)) ) { concatModeChar = cliArgs[i][0] } if (concatModeChar && cliArgs[i].endsWith(concatModeChar)) { concatModeChar = false } } return combinedArgs } const getNamedArguments = (cliArgs) => { const args = arg(namedArguments, { permissive: true, argv: cliArgs, }) debug('initial parsed arguments %o', args) return { expect: args['--expect'], proxyHost: args['--proxy-host'], proxyUser: args['--proxy-user'], proxyPassword: args['--proxy-password'], proxyPort: args['--proxy-port'], proxyProtocol: args['--proxy-protocol'], } } /** * Returns parsed command line arguments. * If start command is npm script name defined in the package.json * file in the current working directory, returns 'npm run start' command. */ const getArguments = (cliArgs) => { la(is.strings(cliArgs), 'expected list of strings', cliArgs) const service = { start: 'start', url: undefined, } const services = [service] let test = 'test' if (cliArgs.length === 1 && isUrlOrPort(cliArgs[0])) { // passed just single url or port number, for example // "start": "http://localhost:8080" service.url = normalizeUrl(cliArgs[0]) } else if (cliArgs.length === 2) { if (isUrlOrPort(cliArgs[0])) { // passed port and custom test command // like ":8080 test-ci" service.url = normalizeUrl(cliArgs[0]) test = cliArgs[1] } if (isUrlOrPort(cliArgs[1])) { // passed start command and url/port // like "start-server 8080" service.start = cliArgs[0] service.url = normalizeUrl(cliArgs[1]) } } else if (cliArgs.length === 5) { service.start = cliArgs[0] service.url = normalizeUrl(cliArgs[1]) const secondService = { start: cliArgs[2], url: normalizeUrl(cliArgs[3]), } services.push(secondService) test = cliArgs[4] } else { la( cliArgs.length === 3, 'expected <npm script name that starts server> <url or port> <npm script name that runs tests>\n', 'example: start-test start 8080 test\n', 'see https://github.com/bahmutov/start-server-and-test#use\n', ) service.start = cliArgs[0] service.url = normalizeUrl(cliArgs[1]) test = cliArgs[2] } services.forEach((service) => { service.start = normalizeCommand(service.start) }) test = normalizeCommand(test) return { services, test, } } function normalizeCommand(command) { return UTILS.isPackageScriptName(command) ? `npm run ${command}` : command } /** * Returns true if the given string is a name of a script in the package.json file * in the current working directory */ const isPackageScriptName = (command) => { la( is.unemptyString(command), 'expected command name string', command, ) const packageFilename = join(process.cwd(), 'package.json') if (!existsSync(packageFilename)) { return false } const packageJson = require(packageFilename) if (!packageJson.scripts) { return false } return Boolean(packageJson.scripts[command]) } const isWaitOnUrl = (s) => /^https?-(?:get|head|options)/.test(s) const isUrlOrPort = (input) => { const str = is.string(input) ? input.split('|') : [input] return str.every((s) => { if (is.url(s)) { return s } // wait-on allows specifying HTTP verb to use instead of default HEAD // and the format then is like "http-get://domain.com" to use GET if (isWaitOnUrl(s)) { return s } if (is.number(s)) { return is.port(s) } if (!is.string(s)) { return false } if (s[0] === ':') { const withoutColon = s.substr(1) return is.port(parseInt(withoutColon)) } return is.port(parseInt(s)) }) } /** * Returns the host to ping if the user specified just the port. * For a long time, the safest bet was "localhost", but now modern * web servers seem to bind to "0.0.0.0", which means * the "127.0.0.1" works better */ const getHost = () => '127.0.0.1' const normalizeUrl = (input) => { const str = is.string(input) ? input.split('|') : [input] const defaultHost = getHost() return str.map((s) => { if (is.url(s)) { return s } if (is.number(s) && is.port(s)) { return `http://${defaultHost}:${s}` } if (!is.string(s)) { return s } if ( s.startsWith('localhost') || s.startsWith('127.0.0.1') || s.startsWith('0.0.0.0') ) { return `http://${s}` } if (is.port(parseInt(s))) { return `http://${defaultHost}:${s}` } if (s[0] === ':') { return `http://${defaultHost}${s}` } // for anything else, return original argument return s }) } function printArguments({ services, test, namedArguments }) { la( is.number(namedArguments.expect), 'expected status code should be a number', namedArguments.expect, ) services.forEach((service, k) => { console.log( '%d: starting server using command "%s"', k + 1, service.start, ) console.log( 'and when url "%s" is responding with HTTP status code %d', service.url, namedArguments.expect, ) }) if (process.env.WAIT_ON_INTERVAL !== undefined) { console.log( 'WAIT_ON_INTERVAL is set to', process.env.WAIT_ON_INTERVAL, ) } if (process.env.WAIT_ON_TIMEOUT !== undefined) { console.log( 'WAIT_ON_TIMEOUT is set to', process.env.WAIT_ON_TIMEOUT, ) } console.log('running tests using command "%s"', test) console.log('') } // placing functions into a common object // makes them methods for easy stubbing const UTILS = { crossArguments, getArguments, getNamedArguments, isPackageScriptName, isUrlOrPort, normalizeUrl, printArguments, } module.exports = UTILS