1 | const la = require('lazy-ass')
|
2 | const is = require('check-more-types')
|
3 | const { join } = require('path')
|
4 | const { existsSync } = require('fs')
|
5 | const arg = require('arg')
|
6 | const debug = require('debug')('start-server-and-test')
|
7 |
|
8 | const namedArguments = {
|
9 | '--expect': Number
|
10 | }
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 | const crossArguments = cliArguments => {
|
18 | const args = arg(namedArguments, {
|
19 | permissive: true,
|
20 | argv: cliArguments
|
21 | })
|
22 | debug('initial parsed arguments %o', args)
|
23 |
|
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 |
|
60 | const 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 |
|
69 | '--expected': '--expect'
|
70 | }
|
71 | }
|
72 |
|
73 |
|
74 |
|
75 |
|
76 |
|
77 |
|
78 | const 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 |
|
91 |
|
92 | service.url = normalizeUrl(cliArgs[0])
|
93 | } else if (cliArgs.length === 2) {
|
94 | if (isUrlOrPort(cliArgs[0])) {
|
95 |
|
96 |
|
97 | service.url = normalizeUrl(cliArgs[0])
|
98 | test = cliArgs[1]
|
99 | }
|
100 | if (isUrlOrPort(cliArgs[1])) {
|
101 |
|
102 |
|
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 |
|
141 | function normalizeCommand (command) {
|
142 | return UTILS.isPackageScriptName(command) ? `npm run ${command}` : command
|
143 | }
|
144 |
|
145 |
|
146 |
|
147 |
|
148 |
|
149 | const 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 |
|
163 | const isWaitOnUrl = s => /^https?-(?:get|head|options)/.test(s)
|
164 |
|
165 | const 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 |
|
173 |
|
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 |
|
194 |
|
195 |
|
196 |
|
197 |
|
198 | const getHost = () => '127.0.0.1'
|
199 |
|
200 | const 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 |
|
233 | return s
|
234 | })
|
235 | }
|
236 |
|
237 | function 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 |
|
266 |
|
267 | const UTILS = {
|
268 | crossArguments,
|
269 | getArguments,
|
270 | getNamedArguments,
|
271 | isPackageScriptName,
|
272 | isUrlOrPort,
|
273 | normalizeUrl,
|
274 | printArguments
|
275 | }
|
276 |
|
277 | module.exports = UTILS
|