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