]>
Commit | Line | Data |
---|---|---|
531c751f | 1 | 'use strict'; |
2 | ||
3 | const path = require('path'); | |
4 | ||
5 | const Promise = require('bluebird'); | |
6 | ||
7 | const fs = Promise.promisifyAll(require('fs')); | |
8 | ||
9 | const retryPromise = require('promise-retry'); | |
10 | ||
11 | const spawn = require('cross-spawn'); | |
12 | ||
13 | const colors = require('chalk'); | |
14 | ||
15 | const debug = require('debug')('purs-loader'); | |
16 | ||
17 | const dargs = require('./dargs'); | |
18 | ||
19 | const Psc = require('./Psc'); | |
20 | ||
21 | const PsModuleMap = require('./PsModuleMap'); | |
22 | ||
23 | function connect(psModule) { | |
24 | const options = psModule.options | |
25 | const cache = psModule.cache | |
26 | ||
27 | if (cache.ideServer) return Promise.resolve(psModule) | |
28 | ||
29 | cache.ideServer = true | |
30 | ||
31 | const connect_ = () => new Promise((resolve, reject) => { | |
32 | const args = dargs(options.pscIdeArgs) | |
33 | ||
34 | debug('attempting to connect to psc-ide-server', args) | |
35 | ||
36 | const ideClient = spawn('psc-ide-client', args) | |
37 | ||
38 | ideClient.stderr.on('data', data => { | |
39 | debug(data.toString()) | |
40 | cache.ideServer = false | |
4b99e432 | 41 | reject(new Error('psc-ide-client failed')) |
531c751f | 42 | }) |
43 | ideClient.stdout.once('data', data => { | |
44 | debug(data.toString()) | |
45 | if (data.toString()[0] === '{') { | |
46 | const res = JSON.parse(data.toString()) | |
47 | if (res.resultType === 'success') { | |
48 | cache.ideServer = ideServer | |
49 | resolve(psModule) | |
50 | } else { | |
51 | cache.ideServer = ideServer | |
4b99e432 | 52 | reject(new Error('psc-ide-client failed')) |
531c751f | 53 | } |
54 | } else { | |
55 | cache.ideServer = false | |
4b99e432 | 56 | reject(new Error('psc-ide-client failed')) |
531c751f | 57 | } |
58 | }) | |
59 | ideClient.stdin.resume() | |
60 | ideClient.stdin.write(JSON.stringify({ command: 'load' })) | |
61 | ideClient.stdin.write('\n') | |
62 | }) | |
63 | ||
6ab1eca0 | 64 | const serverArgs = dargs(Object.assign({ |
531c751f | 65 | outputDirectory: options.output, |
4305f5b0 | 66 | '_': options.src |
6ab1eca0 | 67 | }, options.pscIdeServerArgs)) |
531c751f | 68 | |
6ab1eca0 | 69 | debug('attempting to start psc-ide-server', serverArgs) |
531c751f | 70 | |
6ab1eca0 | 71 | const ideServer = cache.ideServer = spawn('psc-ide-server', serverArgs) |
4305f5b0 | 72 | |
73 | ideServer.stdout.on('data', data => { | |
74 | debug('psc-ide-server stdout: %s', data.toString()); | |
75 | }); | |
76 | ||
531c751f | 77 | ideServer.stderr.on('data', data => { |
4305f5b0 | 78 | debug('psc-ide-server stderr: %s', data.toString()); |
79 | }); | |
80 | ||
81 | ideServer.on('error', error => { | |
82 | debug('psc-ide-server error: %o', error); | |
83 | }); | |
84 | ||
85 | ideServer.on('close', (code, signal) => { | |
86 | debug('psc-ide-server close: %s %s', code, signal); | |
87 | }); | |
531c751f | 88 | |
89 | return retryPromise((retry, number) => { | |
90 | return connect_().catch(error => { | |
91 | if (!cache.ideServer && number === 9) { | |
92 | debug(error) | |
93 | ||
df05d7e1 | 94 | console.warn('Failed to connect to or start psc-ide-server. A full compilation will occur on rebuild'); |
531c751f | 95 | |
96 | return Promise.resolve(psModule) | |
97 | } | |
98 | ||
99 | return retry(error) | |
100 | }) | |
101 | }, { | |
102 | retries: 9, | |
103 | factor: 1, | |
104 | minTimeout: 333, | |
105 | maxTimeout: 333, | |
106 | }) | |
107 | } | |
108 | module.exports.connect = connect; | |
109 | ||
110 | function rebuild(psModule) { | |
111 | const options = psModule.options | |
112 | const cache = psModule.cache | |
113 | ||
114 | debug('attempting rebuild with psc-ide-client %s', psModule.srcPath) | |
115 | ||
116 | const request = (body) => new Promise((resolve, reject) => { | |
117 | const args = dargs(options.pscIdeArgs) | |
118 | const ideClient = spawn('psc-ide-client', args) | |
119 | ||
120 | var stdout = '' | |
121 | var stderr = '' | |
122 | ||
123 | ideClient.stdout.on('data', data => { | |
124 | stdout = stdout + data.toString() | |
125 | }) | |
126 | ||
127 | ideClient.stderr.on('data', data => { | |
128 | stderr = stderr + data.toString() | |
129 | }) | |
130 | ||
131 | ideClient.on('close', code => { | |
132 | if (code !== 0) { | |
133 | const error = stderr === '' ? 'Failed to spawn psc-ide-client' : stderr | |
134 | return reject(new Error(error)) | |
135 | } | |
136 | ||
137 | let res = null | |
138 | ||
139 | try { | |
140 | res = JSON.parse(stdout.toString()) | |
141 | debug(res) | |
142 | } catch (err) { | |
143 | return reject(err) | |
144 | } | |
145 | ||
146 | if (res && !Array.isArray(res.result)) { | |
df05d7e1 | 147 | return resolve(psModule); |
531c751f | 148 | } |
149 | ||
150 | Promise.map(res.result, (item, i) => { | |
151 | debug(item) | |
152 | return formatIdeResult(item, options, i, res.result.length) | |
153 | }) | |
154 | .then(compileMessages => { | |
155 | if (res.resultType === 'error') { | |
28124e28 | 156 | if (res.result.some(item => { |
157 | const isUnknownModule = item.errorCode === 'UnknownModule'; | |
158 | ||
159 | const isUnknownModuleImport = item.errorCode === 'UnknownName' && /Unknown module/.test(item.message); | |
160 | ||
161 | return isUnknownModule || isUnknownModuleImport; | |
162 | })) { | |
531c751f | 163 | debug('unknown module, attempting full recompile') |
164 | return Psc.compile(psModule) | |
165 | .then(() => PsModuleMap.makeMap(options.src).then(map => { | |
4305f5b0 | 166 | debug('rebuilt module map after unknown module forced a recompile'); |
531c751f | 167 | cache.psModuleMap = map; |
168 | })) | |
169 | .then(() => request({ command: 'load' })) | |
170 | .then(resolve) | |
df05d7e1 | 171 | .catch(() => resolve(psModule)) |
531c751f | 172 | } |
45c62a2c | 173 | const errorMessage = compileMessages.join('\n'); |
174 | if (errorMessage.length) { | |
175 | psModule.emitError(errorMessage); | |
176 | } | |
df05d7e1 | 177 | resolve(psModule); |
531c751f | 178 | } else { |
45c62a2c | 179 | const warningMessage = compileMessages.join('\n'); |
180 | if (options.warnings && warningMessage.length) { | |
181 | psModule.emitWarning(warningMessage); | |
182 | } | |
183 | resolve(psModule); | |
531c751f | 184 | } |
185 | }) | |
186 | }) | |
187 | ||
4305f5b0 | 188 | debug('psc-ide-client stdin: %o', body); |
189 | ||
531c751f | 190 | ideClient.stdin.write(JSON.stringify(body)) |
191 | ideClient.stdin.write('\n') | |
192 | }) | |
193 | ||
194 | return request({ | |
195 | command: 'rebuild', | |
196 | params: { | |
197 | file: psModule.srcPath, | |
198 | } | |
199 | }) | |
200 | } | |
201 | module.exports.rebuild = rebuild; | |
202 | ||
203 | function formatIdeResult(result, options, index, length) { | |
531c751f | 204 | let numAndErr = `[${index+1}/${length} ${result.errorCode}]` |
205 | numAndErr = options.pscIdeColors ? colors.yellow(numAndErr) : numAndErr | |
206 | ||
055d127b | 207 | function makeResult() { |
208 | return Promise.resolve(`\n${numAndErr} ${result.message}`) | |
209 | } | |
210 | ||
211 | function makeResultSnippet(filename, pos) { | |
212 | const srcPath = path.relative(options.context, filename); | |
213 | const fileAndPos = `${srcPath}:${pos.startLine}:${pos.startColumn}` | |
214 | ||
215 | return fs.readFileAsync(filename, 'utf8').then(source => { | |
216 | const lines = source.split('\n').slice(pos.startLine - 1, pos.endLine) | |
217 | const endsOnNewline = pos.endColumn === 1 && pos.startLine !== pos.endLine | |
218 | const up = options.pscIdeColors ? colors.red('^') : '^' | |
219 | const down = options.pscIdeColors ? colors.red('v') : 'v' | |
220 | let trimmed = lines.slice(0) | |
221 | ||
222 | if (endsOnNewline) { | |
223 | lines.splice(lines.length - 1, 1) | |
224 | pos.endLine = pos.endLine - 1 | |
225 | pos.endColumn = lines[lines.length - 1].length || 1 | |
226 | } | |
531c751f | 227 | |
055d127b | 228 | // strip newlines at the end |
229 | if (endsOnNewline) { | |
230 | trimmed = lines.reverse().reduce((trimmed, line, i) => { | |
231 | if (i === 0 && line === '') trimmed.trimming = true | |
232 | if (!trimmed.trimming) trimmed.push(line) | |
233 | if (trimmed.trimming && line !== '') { | |
234 | trimmed.trimming = false | |
235 | trimmed.push(line) | |
236 | } | |
237 | return trimmed | |
238 | }, []).reverse() | |
239 | pos.endLine = pos.endLine - (lines.length - trimmed.length) | |
240 | pos.endColumn = trimmed[trimmed.length - 1].length || 1 | |
241 | } | |
531c751f | 242 | |
055d127b | 243 | const spaces = ' '.repeat(String(pos.endLine).length) |
244 | let snippet = trimmed.map((line, i) => { | |
245 | return ` ${pos.startLine + i} ${line}` | |
246 | }).join('\n') | |
531c751f | 247 | |
055d127b | 248 | if (trimmed.length === 1) { |
249 | snippet += `\n ${spaces} ${' '.repeat(pos.startColumn - 1)}${up.repeat(pos.endColumn - pos.startColumn + 1)}` | |
250 | } else { | |
251 | snippet = ` ${spaces} ${' '.repeat(pos.startColumn - 1)}${down}\n${snippet}` | |
252 | snippet += `\n ${spaces} ${' '.repeat(pos.endColumn - 1)}${up}` | |
253 | } | |
531c751f | 254 | |
055d127b | 255 | return Promise.resolve(`\n${numAndErr} ${fileAndPos}\n\n${snippet}\n\n${result.message}`) |
256 | }) | |
257 | } | |
258 | ||
259 | return result.filename && result.position ? makeResultSnippet(result.filename, result.position) : makeResult(); | |
531c751f | 260 | } |