]>
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 | ||
1c12889c | 23 | module.exports.connect = function connect(psModule) { |
531c751f | 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 | ||
1c12889c | 34 | debug('attempting to run purs ide client: %o', args) |
531c751f | 35 | |
1c12889c | 36 | const ideClient = spawn('purs', ['ide', 'client'].concat(args)) |
531c751f | 37 | |
38 | ideClient.stderr.on('data', data => { | |
39 | debug(data.toString()) | |
40 | cache.ideServer = false | |
1c12889c | 41 | reject(new Error('purs 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 | |
1c12889c | 52 | reject(new Error('purs ide client failed')) |
531c751f | 53 | } |
54 | } else { | |
55 | cache.ideServer = false | |
1c12889c | 56 | reject(new Error('purs 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 | |
1c12889c | 69 | debug('attempting to start purs ide server: %o', serverArgs) |
531c751f | 70 | |
1c12889c | 71 | const ideServer = cache.ideServer = spawn('purs', ['ide', 'server'].concat(serverArgs)) |
4305f5b0 | 72 | |
73 | ideServer.stdout.on('data', data => { | |
1c12889c | 74 | debug('purs ide server stdout: %s', data.toString()); |
4305f5b0 | 75 | }); |
76 | ||
531c751f | 77 | ideServer.stderr.on('data', data => { |
1c12889c | 78 | debug('purs ide server stderr: %s', data.toString()); |
4305f5b0 | 79 | }); |
80 | ||
81 | ideServer.on('error', error => { | |
1c12889c | 82 | debug('purs ide server error: %o', error); |
4305f5b0 | 83 | }); |
84 | ||
85 | ideServer.on('close', (code, signal) => { | |
1c12889c | 86 | debug('purs ide server close: %s %s', code, signal); |
4305f5b0 | 87 | }); |
531c751f | 88 | |
89 | return retryPromise((retry, number) => { | |
90 | return connect_().catch(error => { | |
91 | if (!cache.ideServer && number === 9) { | |
92 | debug(error) | |
93 | ||
1c12889c | 94 | console.warn('Failed to connect to or start purs 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 | }) | |
1c12889c | 107 | }; |
531c751f | 108 | |
1c12889c | 109 | module.exports.rebuild = function rebuild(psModule) { |
531c751f | 110 | const options = psModule.options |
111 | const cache = psModule.cache | |
112 | ||
1c12889c | 113 | debug('attempting rebuild with purs ide client %s', psModule.srcPath) |
531c751f | 114 | |
115 | const request = (body) => new Promise((resolve, reject) => { | |
116 | const args = dargs(options.pscIdeArgs) | |
1c12889c | 117 | const ideClient = spawn('purs', ['ide', 'client'].concat(args)) |
531c751f | 118 | |
119 | var stdout = '' | |
120 | var stderr = '' | |
121 | ||
122 | ideClient.stdout.on('data', data => { | |
123 | stdout = stdout + data.toString() | |
124 | }) | |
125 | ||
126 | ideClient.stderr.on('data', data => { | |
127 | stderr = stderr + data.toString() | |
128 | }) | |
129 | ||
130 | ideClient.on('close', code => { | |
131 | if (code !== 0) { | |
1c12889c | 132 | const error = stderr === '' ? 'Failed to spawn purs ide client' : stderr |
531c751f | 133 | return reject(new Error(error)) |
134 | } | |
135 | ||
136 | let res = null | |
137 | ||
138 | try { | |
139 | res = JSON.parse(stdout.toString()) | |
140 | debug(res) | |
141 | } catch (err) { | |
142 | return reject(err) | |
143 | } | |
144 | ||
145 | if (res && !Array.isArray(res.result)) { | |
df05d7e1 | 146 | return resolve(psModule); |
531c751f | 147 | } |
148 | ||
149 | Promise.map(res.result, (item, i) => { | |
150 | debug(item) | |
151 | return formatIdeResult(item, options, i, res.result.length) | |
152 | }) | |
153 | .then(compileMessages => { | |
154 | if (res.resultType === 'error') { | |
28124e28 | 155 | if (res.result.some(item => { |
bf1a6040 | 156 | const isModuleNotFound = item.errorCode === 'ModuleNotFound'; |
157 | ||
28124e28 | 158 | const isUnknownModule = item.errorCode === 'UnknownModule'; |
159 | ||
160 | const isUnknownModuleImport = item.errorCode === 'UnknownName' && /Unknown module/.test(item.message); | |
161 | ||
bf1a6040 | 162 | return isModuleNotFound || isUnknownModule || isUnknownModuleImport; |
28124e28 | 163 | })) { |
531c751f | 164 | debug('unknown module, attempting full recompile') |
165 | return Psc.compile(psModule) | |
166 | .then(() => PsModuleMap.makeMap(options.src).then(map => { | |
4305f5b0 | 167 | debug('rebuilt module map after unknown module forced a recompile'); |
531c751f | 168 | cache.psModuleMap = map; |
169 | })) | |
170 | .then(() => request({ command: 'load' })) | |
171 | .then(resolve) | |
df05d7e1 | 172 | .catch(() => resolve(psModule)) |
531c751f | 173 | } |
45c62a2c | 174 | const errorMessage = compileMessages.join('\n'); |
175 | if (errorMessage.length) { | |
176 | psModule.emitError(errorMessage); | |
177 | } | |
df05d7e1 | 178 | resolve(psModule); |
531c751f | 179 | } else { |
45c62a2c | 180 | const warningMessage = compileMessages.join('\n'); |
181 | if (options.warnings && warningMessage.length) { | |
182 | psModule.emitWarning(warningMessage); | |
183 | } | |
184 | resolve(psModule); | |
531c751f | 185 | } |
186 | }) | |
187 | }) | |
188 | ||
1c12889c | 189 | debug('purs ide client stdin: %o', body); |
4305f5b0 | 190 | |
531c751f | 191 | ideClient.stdin.write(JSON.stringify(body)) |
192 | ideClient.stdin.write('\n') | |
193 | }) | |
194 | ||
195 | return request({ | |
196 | command: 'rebuild', | |
197 | params: { | |
198 | file: psModule.srcPath, | |
199 | } | |
200 | }) | |
1c12889c | 201 | }; |
531c751f | 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 | } |