UNPKG

4.66 kBJavaScriptView Raw
1'use strict'
2
3const spawn = require('child_process').spawn
4const log = require('./log').getInstance()
5const path = require('path')
6
7class Script {
8 constructor (name, code) {
9 this.name = name
10 this.code = code
11 this.done = false
12 this.pid = null
13 this.child = null
14 this.exitCode = 0
15 this.onEnd = null
16 this.duration = 0
17 }
18
19 setName (name) {
20 this.name = name
21 }
22
23 isBackground () {
24 return this.name.includes('+')
25 }
26
27 isPause () {
28 return this.name === '-'
29 }
30
31 end () {
32 this.done = true
33 this.onEnd && this.onEnd()
34 }
35
36 hasEnded () {
37 return this.done === true
38 }
39
40 hasFailed () {
41 return !this.isValid() || this.exitCode !== 0
42 }
43
44 isRunning () {
45 return this.pid !== null && !this.hasEnded()
46 }
47
48 isValid () {
49 return this.code || this.isPause()
50 }
51
52 async start (onEnd) {
53 this.onEnd = onEnd
54
55 // Handle invalid scripts.
56 if (!this.isValid()) {
57 log.err('runna', `Script ${this.name} is not valid.`)
58 return
59 }
60
61 if (this.isPause()) {
62 this.pid = 0
63 return
64 }
65
66 if (this.isRunning()) {
67 return
68 }
69
70 const spawnArgs = this.code.split(' ')
71
72 // Spawn a child process.
73 // Scripts running in the background should not exit on error.
74 return new Promise(resolve => {
75 const timestamp = Date.now()
76
77 // Spawn child process. Use shell to make sure the proper path is used.
78 // This ensures script resolution from either npm or yarn across all
79 // operating systems.
80 log.dbg('runna', `Script ${this.name} started.`)
81 const child = spawn(spawnArgs[0], spawnArgs.slice(1), { shell: true })
82
83 // Finalization handling.
84 let done
85 const end = () => {
86 if (!done) {
87 this.duration = Date.now() - timestamp
88 log.dbg('runna', `Script ${this.name} completed in ${this.duration} ms.`)
89 this.end()
90 done = resolve()
91 }
92 }
93
94 child.on('close', code => {
95 if (code !== 0) {
96 log.err('runna', `Script ${this.name} exited with error code ${code}.`)
97 this.fail(code)
98 }
99
100 end()
101 })
102
103 child.on('error', err => {
104 log.err('runna', `Script ${this.name} threw an error.`)
105 log.err(this.name, err)
106 this.fail(1)
107
108 end()
109 })
110
111 // Capture stderr.
112 child.stderr.on('data', buf => {
113 log.err(this.name, buf)
114 if (!this.isBackground()) {
115 this.fail(1)
116 }
117 })
118
119 // Capture stdout.
120 child.stdout.on('data', buf => {
121 log.dbg(this.name, buf)
122 })
123
124 // Update script
125 this.pid = child.pid
126 this.child = child
127 })
128 }
129
130 _getSpawnArgs (cmd, binaries) {
131 const args = cmd.split(' ')
132 const packageName = args[0]
133
134 // Resolve local package binary.
135 if (binaries[packageName]) {
136 args[0] = binaries[packageName]
137 }
138
139 return [args, false]
140 }
141
142 fail (code) {
143 this.exitCode = code
144 }
145
146 //
147 // Initialization.
148 //
149
150 static getInstances (name, code, files = [], projects = []) {
151 if (!name) {
152 throw new Error('Script must have a name.')
153 }
154
155 const script = new Script(name, code)
156 if (!script.isPause() && !code) {
157 throw new Error(`Unknown script: ${name}`)
158 }
159
160 // Plain script.
161 const scripts = []
162 if (script.isPause() || script._isPlain()) {
163 scripts.push(script)
164 } else if (script._isProjOnly()) {
165 for (const project of projects) {
166 scripts.push(new Script(`${script.name}::${project}`, script.code.replace(/\$PROJ/g, project)))
167 }
168 } else if (script._isFileOnly()) {
169 for (const file of files) {
170 const suffix = file ? `::${path.basename(file)}` : ''
171 scripts.push(new Script(`${script.name}${suffix}`, script.code.replace(/\$FILE/g, file)))
172 }
173 } else if (script._isProjAndFile()) {
174 for (const project of projects) {
175 const projCode = script.code.replace(/\$PROJ/g, project)
176 for (const file of files) {
177 const suffix = file ? `:${path.basename(file)}` : ''
178 scripts.push(new Script(`${script.name}::${project}${suffix}`, projCode.replace(/\$FILE/g, file)))
179 }
180 }
181 }
182
183 return scripts
184 }
185
186 _isPlain (code) {
187 return !this.code.includes('$PROJ') && !this.code.includes('$FILE')
188 }
189
190 _isProjOnly () {
191 return this.code.includes('$PROJ') && !this.code.includes('$FILE')
192 }
193
194 _isFileOnly () {
195 return !this.code.includes('$PROJ') && this.code.includes('$FILE')
196 }
197
198 _isProjAndFile () {
199 return this.code.includes('$PROJ') && this.code.includes('$FILE')
200 }
201}
202
203module.exports = Script