UNPKG

5.38 kBJavaScriptView Raw
1const la = require('lazy-ass')
2const is = require('check-more-types')
3const { join } = require('path')
4const { existsSync } = require('fs')
5
6/**
7 * Returns new array of command line arguments
8 * where leading and trailing " and ' are indicating
9 * the beginning and end of an argument.
10 */
11const crossArguments = cliArgs => {
12 let concatModeChar = false
13 const indicationChars = ["'", '"', '`']
14 const combinedArgs = []
15 for (let i = 0; i < cliArgs.length; i++) {
16 let arg = cliArgs[i]
17 if (
18 !concatModeChar &&
19 indicationChars.some(char => cliArgs[i].startsWith(char))
20 ) {
21 arg = arg.slice(1)
22 }
23 if (concatModeChar && cliArgs[i].endsWith(concatModeChar)) {
24 arg = arg.slice(0, -1)
25 }
26
27 if (concatModeChar && combinedArgs.length) {
28 combinedArgs[combinedArgs.length - 1] += ' ' + arg
29 } else {
30 combinedArgs.push(arg)
31 }
32
33 if (
34 !concatModeChar &&
35 indicationChars.some(char => cliArgs[i].startsWith(char))
36 ) {
37 concatModeChar = cliArgs[i][0]
38 }
39 if (concatModeChar && cliArgs[i].endsWith(concatModeChar)) {
40 concatModeChar = false
41 }
42 }
43 return combinedArgs
44}
45
46/**
47 * Returns parsed command line arguments.
48 * If start command is NPM script name defined in the package.json
49 * file in the current working directory, returns 'npm run start' command.
50 */
51const getArguments = cliArgs => {
52 la(is.strings(cliArgs), 'expected list of strings', cliArgs)
53
54 const service = {
55 start: 'start',
56 url: undefined
57 }
58 const services = [service]
59
60 let test = 'test'
61
62 if (cliArgs.length === 1 && isUrlOrPort(cliArgs[0])) {
63 // passed just single url or port number, for example
64 // "start": "http://localhost:8080"
65 service.url = normalizeUrl(cliArgs[0])
66 } else if (cliArgs.length === 2) {
67 if (isUrlOrPort(cliArgs[0])) {
68 // passed port and custom test command
69 // like ":8080 test-ci"
70 service.url = normalizeUrl(cliArgs[0])
71 test = cliArgs[1]
72 }
73 if (isUrlOrPort(cliArgs[1])) {
74 // passed start command and url/port
75 // like "start-server 8080"
76 service.start = cliArgs[0]
77 service.url = normalizeUrl(cliArgs[1])
78 }
79 } else if (cliArgs.length === 5) {
80 service.start = cliArgs[0]
81 service.url = normalizeUrl(cliArgs[1])
82
83 const secondService = {
84 start: cliArgs[2],
85 url: normalizeUrl(cliArgs[3])
86 }
87 services.push(secondService)
88
89 test = cliArgs[4]
90 } else {
91 la(
92 cliArgs.length === 3,
93 'expected <NPM script name that starts server> <url or port> <NPM script name that runs tests>\n',
94 'example: start-test start 8080 test\n',
95 'see https://github.com/bahmutov/start-server-and-test#use\n'
96 )
97 service.start = cliArgs[0]
98 service.url = normalizeUrl(cliArgs[1])
99 test = cliArgs[2]
100 }
101
102 services.forEach(service => {
103 service.start = normalizeCommand(service.start)
104 })
105
106 test = normalizeCommand(test)
107
108 return {
109 services,
110 test
111 }
112}
113
114function normalizeCommand (command) {
115 return UTILS.isPackageScriptName(command) ? `npm run ${command}` : command
116}
117
118/**
119 * Returns true if the given string is a name of a script in the package.json file
120 * in the current working directory
121 */
122const isPackageScriptName = command => {
123 la(is.unemptyString(command), 'expected command name string', command)
124
125 const packageFilename = join(process.cwd(), 'package.json')
126 if (!existsSync(packageFilename)) {
127 return false
128 }
129 const packageJson = require(packageFilename)
130 if (!packageJson.scripts) {
131 return false
132 }
133 return Boolean(packageJson.scripts[command])
134}
135
136const isWaitOnUrl = s => /^https?-(?:get|head|options)/.test(s)
137
138const isUrlOrPort = input => {
139 const str = is.string(input) ? input.split('|') : [input]
140
141 return str.every(s => {
142 if (is.url(s)) {
143 return s
144 }
145 // wait-on allows specifying HTTP verb to use instead of default HEAD
146 // and the format then is like "http-get://domain.com" to use GET
147 if (isWaitOnUrl(s)) {
148 return s
149 }
150
151 if (is.number(s)) {
152 return is.port(s)
153 }
154 if (!is.string(s)) {
155 return false
156 }
157 if (s[0] === ':') {
158 const withoutColon = s.substr(1)
159 return is.port(parseInt(withoutColon))
160 }
161 return is.port(parseInt(s))
162 })
163}
164
165const normalizeUrl = input => {
166 const str = is.string(input) ? input.split('|') : [input]
167
168 return str.map(s => {
169 if (is.url(s)) {
170 return s
171 }
172
173 if (is.number(s) && is.port(s)) {
174 return `http://localhost:${s}`
175 }
176
177 if (!is.string(s)) {
178 return s
179 }
180
181 if (is.port(parseInt(s))) {
182 return `http://localhost:${s}`
183 }
184
185 if (s[0] === ':') {
186 return `http://localhost${s}`
187 }
188 // for anything else, return original argument
189 return s
190 })
191}
192
193function printArguments ({ services, test }) {
194 services.forEach((service, k) => {
195 console.log('%d: starting server using command "%s"', k + 1, service.start)
196 console.log(
197 'and when url "%s" is responding with HTTP status code 200',
198 service.url
199 )
200 })
201
202 console.log('running tests using command "%s"', test)
203 console.log('')
204}
205
206// placing functions into a common object
207// makes them methods for easy stubbing
208const UTILS = {
209 crossArguments,
210 getArguments,
211 isPackageScriptName,
212 isUrlOrPort,
213 normalizeUrl,
214 printArguments
215}
216
217module.exports = UTILS