]>
Commit | Line | Data |
---|---|---|
d3f40b6f CS |
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 | }; |