diff options
Diffstat (limited to 'src/ide.js')
-rw-r--r-- | src/ide.js | 324 |
1 files changed, 167 insertions, 157 deletions
@@ -12,193 +12,71 @@ const spawn = require('cross-spawn'); | |||
12 | 12 | ||
13 | const colors = require('chalk'); | 13 | const colors = require('chalk'); |
14 | 14 | ||
15 | const debug = require('debug')('purs-loader'); | 15 | const debug_ = require('debug'); |
16 | 16 | ||
17 | const dargs = require('./dargs'); | 17 | const debug = debug_('purs-loader'); |
18 | |||
19 | const Psc = require('./Psc'); | ||
20 | |||
21 | const PsModuleMap = require('./PsModuleMap'); | ||
22 | |||
23 | module.exports.connect = function connect(psModule) { | ||
24 | const options = psModule.options | ||
25 | const cache = psModule.cache | ||
26 | |||
27 | if (cache.ideServer) return Promise.resolve(psModule) | ||
28 | 18 | ||
29 | cache.ideServer = true | 19 | const debugVerbose = debug_('purs-loader:verbose'); |
30 | 20 | ||
31 | const connect_ = () => new Promise((resolve, reject) => { | 21 | const dargs = require('./dargs'); |
32 | const args = dargs(options.pscIdeArgs) | ||
33 | |||
34 | debug('attempting to run purs ide client: %o', args) | ||
35 | 22 | ||
36 | const ideClient = spawn('purs', ['ide', 'client'].concat(args)) | 23 | const compile = require('./compile'); |
37 | 24 | ||
38 | ideClient.stderr.on('data', data => { | 25 | const PsModuleMap = require('./PsModuleMap'); |
39 | debug(data.toString()) | ||
40 | cache.ideServer = false | ||
41 | reject(new Error('purs ide client failed')) | ||
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 | ||
52 | reject(new Error('purs ide client failed')) | ||
53 | } | ||
54 | } else { | ||
55 | cache.ideServer = false | ||
56 | reject(new Error('purs ide client failed')) | ||
57 | } | ||
58 | }) | ||
59 | ideClient.stdin.resume() | ||
60 | ideClient.stdin.write(JSON.stringify({ command: 'load' })) | ||
61 | ideClient.stdin.write('\n') | ||
62 | }) | ||
63 | 26 | ||
64 | const serverArgs = dargs(Object.assign({ | 27 | function UnknownModuleError() { |
65 | outputDirectory: options.output, | 28 | this.name = 'UnknownModuleError'; |
66 | '_': options.src | 29 | this.stack = (new Error()).stack; |
67 | }, options.pscIdeServerArgs)) | 30 | } |
68 | 31 | ||
69 | debug('attempting to start purs ide server: %o', serverArgs) | 32 | UnknownModuleError.prototype = Object.create(Error.prototype); |
70 | 33 | ||
71 | const ideServer = cache.ideServer = spawn('purs', ['ide', 'server'].concat(serverArgs)) | 34 | UnknownModuleError.prototype.constructor = UnknownModuleError; |
72 | 35 | ||
73 | ideServer.stdout.on('data', data => { | 36 | module.exports.UnknownModuleError = UnknownModuleError; |
74 | debug('purs ide server stdout: %s', data.toString()); | ||
75 | }); | ||
76 | 37 | ||
77 | ideServer.stderr.on('data', data => { | 38 | function spawnIdeClient(body, options) { |
78 | debug('purs ide server stderr: %s', data.toString()); | 39 | const ideClientCommand = 'purs'; |
79 | }); | ||
80 | 40 | ||
81 | ideServer.on('error', error => { | 41 | const ideClientArgs = ['ide', 'client'].concat(dargs(options.pscIdeArgs)); |
82 | debug('purs ide server error: %o', error); | ||
83 | }); | ||
84 | 42 | ||
85 | ideServer.on('close', (code, signal) => { | 43 | const stderr = []; |
86 | debug('purs ide server close: %s %s', code, signal); | ||
87 | }); | ||
88 | 44 | ||
89 | return retryPromise((retry, number) => { | 45 | const stdout = []; |
90 | return connect_().catch(error => { | ||
91 | if (!cache.ideServer && number === 9) { | ||
92 | debug(error) | ||
93 | 46 | ||
94 | console.warn('Failed to connect to or start purs ide server. A full compilation will occur on rebuild'); | 47 | debug('ide client %s %o %o', ideClientCommand, ideClientArgs, body); |
95 | 48 | ||
96 | return Promise.resolve(psModule) | 49 | return new Promise((resolve, reject) => { |
97 | } | 50 | const ideClient = spawn(ideClientCommand, ideClientArgs); |
98 | 51 | ||
99 | return retry(error) | 52 | ideClient.stderr.on('data', data => { |
53 | stderr.push(data.toString()); | ||
100 | }) | 54 | }) |
101 | }, { | ||
102 | retries: 9, | ||
103 | factor: 1, | ||
104 | minTimeout: 333, | ||
105 | maxTimeout: 333, | ||
106 | }) | ||
107 | }; | ||
108 | |||
109 | module.exports.rebuild = function rebuild(psModule) { | ||
110 | const options = psModule.options | ||
111 | const cache = psModule.cache | ||
112 | |||
113 | debug('attempting rebuild with purs ide client %s', psModule.srcPath) | ||
114 | |||
115 | const request = (body) => new Promise((resolve, reject) => { | ||
116 | const args = dargs(options.pscIdeArgs) | ||
117 | const ideClient = spawn('purs', ['ide', 'client'].concat(args)) | ||
118 | |||
119 | var stdout = '' | ||
120 | var stderr = '' | ||
121 | 55 | ||
122 | ideClient.stdout.on('data', data => { | 56 | ideClient.stdout.on('data', data => { |
123 | stdout = stdout + data.toString() | 57 | stdout.push(data.toString()); |
124 | }) | ||
125 | |||
126 | ideClient.stderr.on('data', data => { | ||
127 | stderr = stderr + data.toString() | ||
128 | }) | 58 | }) |
129 | 59 | ||
130 | ideClient.on('close', code => { | 60 | ideClient.on('close', code => { |
131 | if (code !== 0) { | 61 | if (code !== 0) { |
132 | const error = stderr === '' ? 'Failed to spawn purs ide client' : stderr | 62 | const errorMessage = stderr.join(''); |
133 | return reject(new Error(error)) | ||
134 | } | ||
135 | |||
136 | let res = null | ||
137 | 63 | ||
138 | try { | 64 | reject(new Error(`ide client failed: ${errorMessage}`)); |
139 | res = JSON.parse(stdout.toString()) | ||
140 | debug(res) | ||
141 | } catch (err) { | ||
142 | return reject(err) | ||
143 | } | 65 | } |
66 | else { | ||
67 | const result = stdout.join(''); | ||
144 | 68 | ||
145 | if (res && !Array.isArray(res.result)) { | 69 | resolve(result); |
146 | return resolve(psModule); | ||
147 | } | 70 | } |
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') { | ||
155 | if (res.result.some(item => { | ||
156 | const isModuleNotFound = item.errorCode === 'ModuleNotFound'; | ||
157 | |||
158 | const isUnknownModule = item.errorCode === 'UnknownModule'; | ||
159 | |||
160 | const isUnknownModuleImport = item.errorCode === 'UnknownName' && /Unknown module/.test(item.message); | ||
161 | |||
162 | return isModuleNotFound || isUnknownModule || isUnknownModuleImport; | ||
163 | })) { | ||
164 | debug('unknown module, attempting full recompile') | ||
165 | return Psc.compile(psModule) | ||
166 | .then(() => PsModuleMap.makeMap(options.src).then(map => { | ||
167 | debug('rebuilt module map after unknown module forced a recompile'); | ||
168 | cache.psModuleMap = map; | ||
169 | })) | ||
170 | .then(() => request({ command: 'load' })) | ||
171 | .then(resolve) | ||
172 | .catch(() => resolve(psModule)) | ||
173 | } | ||
174 | const errorMessage = compileMessages.join('\n'); | ||
175 | if (errorMessage.length) { | ||
176 | psModule.emitError(errorMessage); | ||
177 | } | ||
178 | resolve(psModule); | ||
179 | } else { | ||
180 | const warningMessage = compileMessages.join('\n'); | ||
181 | if (options.warnings && warningMessage.length) { | ||
182 | psModule.emitWarning(warningMessage); | ||
183 | } | ||
184 | resolve(psModule); | ||
185 | } | ||
186 | }) | ||
187 | }) | 71 | }) |
188 | 72 | ||
189 | debug('purs ide client stdin: %o', body); | 73 | ideClient.stdin.resume(); |
190 | 74 | ||
191 | ideClient.stdin.write(JSON.stringify(body)) | 75 | ideClient.stdin.write(JSON.stringify(body)); |
192 | ideClient.stdin.write('\n') | ||
193 | }) | ||
194 | 76 | ||
195 | return request({ | 77 | ideClient.stdin.write('\n'); |
196 | command: 'rebuild', | 78 | }); |
197 | params: { | 79 | } |
198 | file: psModule.srcPath, | ||
199 | } | ||
200 | }) | ||
201 | }; | ||
202 | 80 | ||
203 | function formatIdeResult(result, options, index, length) { | 81 | function formatIdeResult(result, options, index, length) { |
204 | let numAndErr = `[${index+1}/${length} ${result.errorCode}]` | 82 | let numAndErr = `[${index+1}/${length} ${result.errorCode}]` |
@@ -253,8 +131,140 @@ function formatIdeResult(result, options, index, length) { | |||
253 | } | 131 | } |
254 | 132 | ||
255 | return Promise.resolve(`\n${numAndErr} ${fileAndPos}\n\n${snippet}\n\n${result.message}`) | 133 | return Promise.resolve(`\n${numAndErr} ${fileAndPos}\n\n${snippet}\n\n${result.message}`) |
256 | }) | 134 | }).catch(error => { |
135 | debug('failed to format ide result: %o', error); | ||
136 | |||
137 | return Promise.resolve(''); | ||
138 | }); | ||
257 | } | 139 | } |
258 | 140 | ||
259 | return result.filename && result.position ? makeResultSnippet(result.filename, result.position) : makeResult(); | 141 | return result.filename && result.position ? makeResultSnippet(result.filename, result.position) : makeResult(); |
260 | } | 142 | } |
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 | }; | ||