UNPKG

6.86 kBJavaScriptView Raw
1const la = require('lazy-ass')
2const is = require('check-more-types')
3const { join } = require('path')
4const { existsSync } = require('fs')
5const arg = require('arg')
6const debug = require('debug')('start-server-and-test')
7
8const namedArguments = {
9 '--expect': Number
10}
11
12/**
13 * Returns new array of command line arguments
14 * where leading and trailing " and ' are indicating
15 * the beginning and end of an argument.
16 */
17const crossArguments = cliArguments => {
18 const args = arg(namedArguments, {
19 permissive: true,
20 argv: cliArguments
21 })
22 debug('initial parsed arguments %o', args)
23 // all other arguments
24 const cliArgs = args._
25
26 let concatModeChar = false
27 const indicationChars = ["'", '"', '`']
28 const combinedArgs = []
29 for (let i = 0; i < cliArgs.length; i++) {
30 let arg = cliArgs[i]
31 if (
32 !concatModeChar &&
33 indicationChars.some(char => cliArgs[i].startsWith(char))
34 ) {
35 arg = arg.slice(1)
36 }
37 if (concatModeChar && cliArgs[i].endsWith(concatModeChar)) {
38 arg = arg.slice(0, -1)
39 }
40
41 if (concatModeChar && combinedArgs.length) {
42 combinedArgs[combinedArgs.length - 1] += ' ' + arg
43 } else {
44 combinedArgs.push(arg)
45 }
46
47 if (
48 !concatModeChar &&
49 indicationChars.some(char => cliArgs[i].startsWith(char))
50 ) {
51 concatModeChar = cliArgs[i][0]
52 }
53 if (concatModeChar && cliArgs[i].endsWith(concatModeChar)) {
54 concatModeChar = false
55 }
56 }
57 return combinedArgs
58}
59
60const getNamedArguments = cliArgs => {
61 const args = arg(namedArguments, {
62 permissive: true,
63 argv: cliArgs
64 })
65 debug('initial parsed arguments %o', args)
66 return {
67 expect: args['--expect'],
68 // aliases
69 '--expected': '--expect'
70 }
71}
72
73/**
74 * Returns parsed command line arguments.
75 * If start command is NPM script name defined in the package.json
76 * file in the current working directory, returns 'npm run start' command.
77 */
78const getArguments = cliArgs => {
79 la(is.strings(cliArgs), 'expected list of strings', cliArgs)
80
81 const service = {
82 start: 'start',
83 url: undefined
84 }
85 const services = [service]
86
87 let test = 'test'
88
89 if (cliArgs.length === 1 && isUrlOrPort(cliArgs[0])) {
90 // passed just single url or port number, for example
91 // "start": "http://localhost:8080"
92 service.url = normalizeUrl(cliArgs[0])
93 } else if (cliArgs.length === 2) {
94 if (isUrlOrPort(cliArgs[0])) {
95 // passed port and custom test command
96 // like ":8080 test-ci"
97 service.url = normalizeUrl(cliArgs[0])
98 test = cliArgs[1]
99 }
100 if (isUrlOrPort(cliArgs[1])) {
101 // passed start command and url/port
102 // like "start-server 8080"
103 service.start = cliArgs[0]
104 service.url = normalizeUrl(cliArgs[1])
105 }
106 } else if (cliArgs.length === 5) {
107 service.start = cliArgs[0]
108 service.url = normalizeUrl(cliArgs[1])
109
110 const secondService = {
111 start: cliArgs[2],
112 url: normalizeUrl(cliArgs[3])
113 }
114 services.push(secondService)
115
116 test = cliArgs[4]
117 } else {
118 la(
119 cliArgs.length === 3,
120 'expected <NPM script name that starts server> <url or port> <NPM script name that runs tests>\n',
121 'example: start-test start 8080 test\n',
122 'see https://github.com/bahmutov/start-server-and-test#use\n'
123 )
124 service.start = cliArgs[0]
125 service.url = normalizeUrl(cliArgs[1])
126 test = cliArgs[2]
127 }
128
129 services.forEach(service => {
130 service.start = normalizeCommand(service.start)
131 })
132
133 test = normalizeCommand(test)
134
135 return {
136 services,
137 test
138 }
139}
140
141function normalizeCommand (command) {
142 return UTILS.isPackageScriptName(command) ? `npm run ${command}` : command
143}
144
145/**
146 * Returns true if the given string is a name of a script in the package.json file
147 * in the current working directory
148 */
149const isPackageScriptName = command => {
150 la(is.unemptyString(command), 'expected command name string', command)
151
152 const packageFilename = join(process.cwd(), 'package.json')
153 if (!existsSync(packageFilename)) {
154 return false
155 }
156 const packageJson = require(packageFilename)
157 if (!packageJson.scripts) {
158 return false
159 }
160 return Boolean(packageJson.scripts[command])
161}
162
163const isWaitOnUrl = s => /^https?-(?:get|head|options)/.test(s)
164
165const isUrlOrPort = input => {
166 const str = is.string(input) ? input.split('|') : [input]
167
168 return str.every(s => {
169 if (is.url(s)) {
170 return s
171 }
172 // wait-on allows specifying HTTP verb to use instead of default HEAD
173 // and the format then is like "http-get://domain.com" to use GET
174 if (isWaitOnUrl(s)) {
175 return s
176 }
177
178 if (is.number(s)) {
179 return is.port(s)
180 }
181 if (!is.string(s)) {
182 return false
183 }
184 if (s[0] === ':') {
185 const withoutColon = s.substr(1)
186 return is.port(parseInt(withoutColon))
187 }
188 return is.port(parseInt(s))
189 })
190}
191
192/**
193 * Returns the host to ping if the user specified just the port.
194 * For a long time, the safest bet was "localhost", but now modern
195 * web servers seem to bind to "0.0.0.0", which means
196 * the "127.0.0.1" works better
197 */
198const getHost = () => '127.0.0.1'
199
200const normalizeUrl = input => {
201 const str = is.string(input) ? input.split('|') : [input]
202 const defaultHost = getHost()
203
204 return str.map(s => {
205 if (is.url(s)) {
206 return s
207 }
208
209 if (is.number(s) && is.port(s)) {
210 return `http://${defaultHost}:${s}`
211 }
212
213 if (!is.string(s)) {
214 return s
215 }
216
217 if (
218 s.startsWith('localhost') ||
219 s.startsWith('127.0.0.1') ||
220 s.startsWith('0.0.0.0')
221 ) {
222 return `http://${s}`
223 }
224
225 if (is.port(parseInt(s))) {
226 return `http://${defaultHost}:${s}`
227 }
228
229 if (s[0] === ':') {
230 return `http://${defaultHost}${s}`
231 }
232 // for anything else, return original argument
233 return s
234 })
235}
236
237function printArguments ({ services, test, namedArguments }) {
238 la(
239 is.number(namedArguments.expect),
240 'expected status code should be a number',
241 namedArguments.expect
242 )
243
244 services.forEach((service, k) => {
245 console.log('%d: starting server using command "%s"', k + 1, service.start)
246 console.log(
247 'and when url "%s" is responding with HTTP status code %d',
248 service.url,
249 namedArguments.expect
250 )
251 })
252
253 if (process.env.WAIT_ON_INTERVAL !== undefined) {
254 console.log('WAIT_ON_INTERVAL is set to', process.env.WAIT_ON_INTERVAL)
255 }
256
257 if (process.env.WAIT_ON_TIMEOUT !== undefined) {
258 console.log('WAIT_ON_TIMEOUT is set to', process.env.WAIT_ON_TIMEOUT)
259 }
260
261 console.log('running tests using command "%s"', test)
262 console.log('')
263}
264
265// placing functions into a common object
266// makes them methods for easy stubbing
267const UTILS = {
268 crossArguments,
269 getArguments,
270 getNamedArguments,
271 isPackageScriptName,
272 isUrlOrPort,
273 normalizeUrl,
274 printArguments
275}
276
277module.exports = UTILS