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