]>
Commit | Line | Data |
---|---|---|
1 | 'use strict'; | |
2 | ||
3 | var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol ? "symbol" : typeof obj; }; | |
4 | ||
5 | var colors = require('chalk'); | |
6 | var debug = require('debug')('purs-loader'); | |
7 | var loaderUtils = require('loader-utils'); | |
8 | var globby = require('globby'); | |
9 | var Promise = require('bluebird'); | |
10 | var fs = Promise.promisifyAll(require('fs')); | |
11 | var spawn = require('child_process').spawn; | |
12 | var path = require('path'); | |
13 | var retryPromise = require('promise-retry'); | |
14 | ||
15 | var psModuleRegex = /(?:^|\n)module\s+([\w\.]+)/i; | |
16 | var requireRegex = /require\(['"]\.\.\/([\w\.]+)['"]\)/g; | |
17 | ||
18 | module.exports = function purescriptLoader(source, map) { | |
19 | var callback = this.async(); | |
20 | var config = this.options; | |
21 | var query = loaderUtils.parseQuery(this.query); | |
22 | var webpackOptions = this.options.purescriptLoader || {}; | |
23 | ||
24 | var options = Object.assign({ | |
25 | context: config.context, | |
26 | psc: 'psc', | |
27 | pscArgs: {}, | |
28 | pscBundle: 'psc-bundle', | |
29 | pscBundleArgs: {}, | |
30 | pscIde: false, | |
31 | pscIdeColors: webpackOptions.psc === 'psa' || query.psc === 'psa', | |
32 | pscIdeArgs: {}, | |
33 | bundleOutput: 'output/bundle.js', | |
34 | bundleNamespace: 'PS', | |
35 | bundle: false, | |
36 | warnings: true, | |
37 | output: 'output', | |
38 | src: [path.join('src', '**', '*.purs'), path.join('bower_components', 'purescript-*', 'src', '**', '*.purs')], | |
39 | ffi: [path.join('src', '**', '*.js'), path.join('bower_components', 'purescript-*', 'src', '**', '*.js')] | |
40 | }, webpackOptions, query); | |
41 | ||
42 | this.cacheable && this.cacheable(); | |
43 | ||
44 | var cache = config.purescriptLoaderCache = config.purescriptLoaderCache || { | |
45 | rebuild: false, | |
46 | deferred: [], | |
47 | bundleModules: [] | |
48 | }; | |
49 | ||
50 | if (!config.purescriptLoaderInstalled) { | |
51 | config.purescriptLoaderInstalled = true; | |
52 | ||
53 | // invalidate loader cache when bundle is marked as invalid (in watch mode) | |
54 | this._compiler.plugin('invalid', function () { | |
55 | cache = config.purescriptLoaderCache = { | |
56 | rebuild: options.pscIde, | |
57 | deferred: [], | |
58 | ideServer: cache.ideServer | |
59 | }; | |
60 | }); | |
61 | ||
62 | // add psc warnings to webpack compilation warnings | |
63 | this._compiler.plugin('after-compile', function (compilation, callback) { | |
64 | if (options.warnings && cache.warnings && cache.warnings.length) { | |
65 | compilation.warnings.unshift('PureScript compilation:\n' + cache.warnings.join('')); | |
66 | } | |
67 | ||
68 | if (cache.errors && cache.errors.length) { | |
69 | compilation.errors.unshift('PureScript compilation:\n' + cache.errors.join('\n')); | |
70 | } | |
71 | ||
72 | callback(); | |
73 | }); | |
74 | } | |
75 | ||
76 | var psModuleName = match(psModuleRegex, source); | |
77 | var psModule = { | |
78 | name: psModuleName, | |
79 | load: function load(js) { | |
80 | return callback(null, js); | |
81 | }, | |
82 | reject: function reject(error) { | |
83 | return callback(error); | |
84 | }, | |
85 | srcPath: this.resourcePath, | |
86 | srcDir: path.dirname(this.resourcePath), | |
87 | jsPath: path.resolve(path.join(options.output, psModuleName, 'index.js')), | |
88 | options: options, | |
89 | cache: cache | |
90 | }; | |
91 | ||
92 | if (options.bundle) { | |
93 | cache.bundleModules.push(psModule.name); | |
94 | } | |
95 | ||
96 | if (cache.rebuild) { | |
97 | return connectIdeServer(psModule).then(rebuild).then(toJavaScript).then(psModule.load).catch(psModule.reject); | |
98 | } | |
99 | ||
100 | if (cache.compilation && cache.compilation.length) { | |
101 | return toJavaScript(psModule).then(psModule.load).catch(psModule.reject); | |
102 | } | |
103 | ||
104 | // We need to wait for compilation to finish before the loaders run so that | |
105 | // references to compiled output are valid. | |
106 | cache.deferred.push(psModule); | |
107 | ||
108 | if (!cache.compilation) { | |
109 | return compile(psModule).then(function () { | |
110 | return Promise.map(cache.deferred, function (psModule) { | |
111 | if (_typeof(cache.ideServer) === 'object') cache.ideServer.kill(); | |
112 | return toJavaScript(psModule).then(psModule.load); | |
113 | }); | |
114 | }).catch(function (error) { | |
115 | cache.deferred[0].reject(error); | |
116 | cache.deferred.slice(1).forEach(function (psModule) { | |
117 | return psModule.reject(true); | |
118 | }); | |
119 | }); | |
120 | } | |
121 | }; | |
122 | ||
123 | // The actual loader is executed *after* purescript compilation. | |
124 | function toJavaScript(psModule) { | |
125 | var options = psModule.options; | |
126 | var cache = psModule.cache; | |
127 | var bundlePath = path.resolve(options.bundleOutput); | |
128 | var jsPath = cache.bundle ? bundlePath : psModule.jsPath; | |
129 | ||
130 | debug('loading JavaScript for', psModule.srcPath); | |
131 | ||
132 | return Promise.props({ | |
133 | js: fs.readFileAsync(jsPath, 'utf8'), | |
134 | psModuleMap: psModuleMap(options.src, cache) | |
135 | }).then(function (result) { | |
136 | var js = ''; | |
137 | ||
138 | if (options.bundle) { | |
139 | // if bundling, return a reference to the bundle | |
140 | js = 'module.exports = require("' + path.relative(psModule.srcDir, options.bundleOutput) + '")["' + psModule.name + '"]'; | |
141 | } else { | |
142 | // replace require paths to output files generated by psc with paths | |
143 | // to purescript sources, which are then also run through this loader. | |
144 | var foreignRequire = 'require("' + path.resolve(path.join(psModule.options.output, psModule.name, 'foreign.js')) + '")'; | |
145 | ||
146 | js = result.js.replace(requireRegex, function (m, p1) { | |
147 | return 'require("' + result.psModuleMap[p1] + '")'; | |
148 | }).replace(/require\(['"]\.\/foreign['"]\)/g, foreignRequire); | |
149 | } | |
150 | ||
151 | return js; | |
152 | }); | |
153 | } | |
154 | ||
155 | function compile(psModule) { | |
156 | var options = psModule.options; | |
157 | var cache = psModule.cache; | |
158 | var stderr = []; | |
159 | ||
160 | if (cache.compilation) return Promise.resolve(cache.compilation); | |
161 | ||
162 | cache.compilation = []; | |
163 | cache.warnings = []; | |
164 | cache.errors = []; | |
165 | ||
166 | var args = dargs(Object.assign({ | |
167 | _: options.src, | |
168 | ffi: options.ffi, | |
169 | output: options.output | |
170 | }, options.pscArgs)); | |
171 | ||
172 | debug('spawning compiler %s %o', options.psc, args); | |
173 | ||
174 | return new Promise(function (resolve, reject) { | |
175 | console.log('\nCompiling PureScript...'); | |
176 | ||
177 | var compilation = spawn(options.psc, args); | |
178 | ||
179 | compilation.stderr.on('data', function (data) { | |
180 | return stderr.push(data.toString()); | |
181 | }); | |
182 | ||
183 | compilation.on('close', function (code) { | |
184 | console.log('Finished compiling PureScript.'); | |
185 | if (code !== 0) { | |
186 | cache.compilation = cache.errors = stderr; | |
187 | reject(true); | |
188 | } else { | |
189 | cache.compilation = cache.warnings = stderr; | |
190 | resolve(psModule); | |
191 | } | |
192 | }); | |
193 | }).then(function (compilerOutput) { | |
194 | if (options.bundle) { | |
195 | return bundle(options, cache).then(function () { | |
196 | return psModule; | |
197 | }); | |
198 | } | |
199 | return psModule; | |
200 | }); | |
201 | } | |
202 | ||
203 | function rebuild(psModule) { | |
204 | var options = psModule.options; | |
205 | var cache = psModule.cache; | |
206 | ||
207 | debug('attempting rebuild with psc-ide-client %s', psModule.srcPath); | |
208 | ||
209 | var request = function request(body) { | |
210 | return new Promise(function (resolve, reject) { | |
211 | var args = dargs(options.pscIdeArgs); | |
212 | var ideClient = spawn('psc-ide-client', args); | |
213 | ||
214 | ideClient.stdout.once('data', function (data) { | |
215 | var res = null; | |
216 | ||
217 | try { | |
218 | res = JSON.parse(data.toString()); | |
219 | debug(res); | |
220 | } catch (err) { | |
221 | return reject(err); | |
222 | } | |
223 | ||
224 | if (res && !Array.isArray(res.result)) { | |
225 | return res.resultType === 'success' ? resolve(psModule) : reject('psc-ide rebuild failed'); | |
226 | } | |
227 | ||
228 | Promise.map(res.result, function (item, i) { | |
229 | debug(item); | |
230 | return formatIdeResult(item, options, i, res.result.length); | |
231 | }).then(function (compileMessages) { | |
232 | if (res.resultType === 'error') { | |
233 | if (res.result.some(function (item) { | |
234 | return item.errorCode === 'UnknownModule'; | |
235 | })) { | |
236 | console.log('Unknown module, attempting full recompile'); | |
237 | return compile(psModule).then(function () { | |
238 | return request({ command: 'load' }); | |
239 | }).then(resolve).catch(function () { | |
240 | return reject('psc-ide rebuild failed'); | |
241 | }); | |
242 | } | |
243 | cache.errors = compileMessages; | |
244 | reject('psc-ide rebuild failed'); | |
245 | } else { | |
246 | cache.warnings = compileMessages; | |
247 | resolve(psModule); | |
248 | } | |
249 | }); | |
250 | }); | |
251 | ||
252 | ideClient.stderr.once('data', function (data) { | |
253 | return reject(data.toString()); | |
254 | }); | |
255 | ||
256 | ideClient.stdin.write(JSON.stringify(body)); | |
257 | ideClient.stdin.write('\n'); | |
258 | }); | |
259 | }; | |
260 | ||
261 | return request({ | |
262 | command: 'rebuild', | |
263 | params: { | |
264 | file: psModule.srcPath | |
265 | } | |
266 | }); | |
267 | } | |
268 | ||
269 | function formatIdeResult(result, options, index, length) { | |
270 | var srcPath = path.relative(options.context, result.filename); | |
271 | var pos = result.position; | |
272 | var fileAndPos = srcPath + ':' + pos.startLine + ':' + pos.startColumn; | |
273 | var numAndErr = '[' + (index + 1) + '/' + length + ' ' + result.errorCode + ']'; | |
274 | numAndErr = options.pscIdeColors ? colors.yellow(numAndErr) : numAndErr; | |
275 | ||
276 | return fs.readFileAsync(result.filename, 'utf8').then(function (source) { | |
277 | var lines = source.split('\n').slice(pos.startLine - 1, pos.endLine); | |
278 | var endsOnNewline = pos.endColumn === 1 && pos.startLine !== pos.endLine; | |
279 | var up = options.pscIdeColors ? colors.red('^') : '^'; | |
280 | var down = options.pscIdeColors ? colors.red('v') : 'v'; | |
281 | var trimmed = lines.slice(0); | |
282 | ||
283 | if (endsOnNewline) { | |
284 | lines.splice(lines.length - 1, 1); | |
285 | pos.endLine = pos.endLine - 1; | |
286 | pos.endColumn = lines[lines.length - 1].length || 1; | |
287 | } | |
288 | ||
289 | // strip newlines at the end | |
290 | if (endsOnNewline) { | |
291 | trimmed = lines.reverse().reduce(function (trimmed, line, i) { | |
292 | if (i === 0 && line === '') trimmed.trimming = true; | |
293 | if (!trimmed.trimming) trimmed.push(line); | |
294 | if (trimmed.trimming && line !== '') { | |
295 | trimmed.trimming = false; | |
296 | trimmed.push(line); | |
297 | } | |
298 | return trimmed; | |
299 | }, []).reverse(); | |
300 | pos.endLine = pos.endLine - (lines.length - trimmed.length); | |
301 | pos.endColumn = trimmed[trimmed.length - 1].length || 1; | |
302 | } | |
303 | ||
304 | var spaces = ' '.repeat(String(pos.endLine).length); | |
305 | var snippet = trimmed.map(function (line, i) { | |
306 | return ' ' + (pos.startLine + i) + ' ' + line; | |
307 | }).join('\n'); | |
308 | ||
309 | if (trimmed.length === 1) { | |
310 | snippet += '\n ' + spaces + ' ' + ' '.repeat(pos.startColumn - 1) + up.repeat(pos.endColumn - pos.startColumn + 1); | |
311 | } else { | |
312 | snippet = ' ' + spaces + ' ' + ' '.repeat(pos.startColumn - 1) + down + '\n' + snippet; | |
313 | snippet += '\n ' + spaces + ' ' + ' '.repeat(pos.endColumn - 1) + up; | |
314 | } | |
315 | ||
316 | return Promise.resolve('\n' + numAndErr + ' ' + fileAndPos + '\n\n' + snippet + '\n\n' + result.message); | |
317 | }); | |
318 | } | |
319 | ||
320 | function bundle(options, cache) { | |
321 | if (cache.bundle) return Promise.resolve(cache.bundle); | |
322 | ||
323 | var stdout = []; | |
324 | var stderr = cache.bundle = []; | |
325 | ||
326 | var args = dargs(Object.assign({ | |
327 | _: [path.join(options.output, '*', '*.js')], | |
328 | output: options.bundleOutput, | |
329 | namespace: options.bundleNamespace | |
330 | }, options.pscBundleArgs)); | |
331 | ||
332 | cache.bundleModules.forEach(function (name) { | |
333 | return args.push('--module', name); | |
334 | }); | |
335 | ||
336 | debug('spawning bundler %s %o', options.pscBundle, args.join(' ')); | |
337 | ||
338 | return new Promise(function (resolve, reject) { | |
339 | console.log('Bundling PureScript...'); | |
340 | ||
341 | var compilation = spawn(options.pscBundle, args); | |
342 | ||
343 | compilation.stdout.on('data', function (data) { | |
344 | return stdout.push(data.toString()); | |
345 | }); | |
346 | compilation.stderr.on('data', function (data) { | |
347 | return stderr.push(data.toString()); | |
348 | }); | |
349 | compilation.on('close', function (code) { | |
350 | if (code !== 0) { | |
351 | cache.errors.concat(stderr); | |
352 | return reject(true); | |
353 | } | |
354 | cache.bundle = stderr; | |
355 | resolve(fs.appendFileAsync('output/bundle.js', 'module.exports = ' + options.bundleNamespace)); | |
356 | }); | |
357 | }); | |
358 | } | |
359 | ||
360 | // map of PS module names to their source path | |
361 | function psModuleMap(globs, cache) { | |
362 | if (cache.psModuleMap) return Promise.resolve(cache.psModuleMap); | |
363 | ||
364 | return globby(globs).then(function (paths) { | |
365 | return Promise.props(paths.reduce(function (map, file) { | |
366 | map[file] = fs.readFileAsync(file, 'utf8'); | |
367 | return map; | |
368 | }, {})).then(function (srcMap) { | |
369 | cache.psModuleMap = Object.keys(srcMap).reduce(function (map, file) { | |
370 | var source = srcMap[file]; | |
371 | var psModuleName = match(psModuleRegex, source); | |
372 | map[psModuleName] = path.resolve(file); | |
373 | return map; | |
374 | }, {}); | |
375 | return cache.psModuleMap; | |
376 | }); | |
377 | }); | |
378 | } | |
379 | ||
380 | function connectIdeServer(psModule) { | |
381 | var options = psModule.options; | |
382 | var cache = psModule.cache; | |
383 | ||
384 | if (cache.ideServer) return Promise.resolve(psModule); | |
385 | ||
386 | cache.ideServer = true; | |
387 | ||
388 | var connect = function connect() { | |
389 | return new Promise(function (resolve, reject) { | |
390 | var args = dargs(options.pscIdeArgs); | |
391 | ||
392 | debug('attempting to connect to psc-ide-server', args); | |
393 | ||
394 | var ideClient = spawn('psc-ide-client', args); | |
395 | ||
396 | ideClient.stderr.on('data', function (data) { | |
397 | debug(data.toString()); | |
398 | cache.ideServer = false; | |
399 | reject(true); | |
400 | }); | |
401 | ideClient.stdout.once('data', function (data) { | |
402 | debug(data.toString()); | |
403 | if (data.toString()[0] === '{') { | |
404 | var res = JSON.parse(data.toString()); | |
405 | if (res.resultType === 'success') { | |
406 | cache.ideServer = ideServer; | |
407 | resolve(psModule); | |
408 | } else { | |
409 | cache.ideServer = ideServer; | |
410 | reject(true); | |
411 | } | |
412 | } else { | |
413 | cache.ideServer = false; | |
414 | reject(true); | |
415 | } | |
416 | }); | |
417 | ideClient.stdin.resume(); | |
418 | ideClient.stdin.write(JSON.stringify({ command: 'load' })); | |
419 | ideClient.stdin.write('\n'); | |
420 | }); | |
421 | }; | |
422 | ||
423 | var args = dargs(Object.assign({ | |
424 | outputDirectory: options.output | |
425 | }, options.pscIdeArgs)); | |
426 | ||
427 | debug('attempting to start psc-ide-server', args); | |
428 | ||
429 | var ideServer = cache.ideServer = spawn('psc-ide-server', []); | |
430 | ideServer.stderr.on('data', function (data) { | |
431 | debug(data.toString()); | |
432 | }); | |
433 | ||
434 | return retryPromise(function (retry, number) { | |
435 | return connect().catch(function (error) { | |
436 | if (!cache.ideServer && number === 9) { | |
437 | debug(error); | |
438 | ||
439 | console.log('failed to connect to or start psc-ide-server, ' + 'full compilation will occur on rebuild'); | |
440 | ||
441 | return Promise.resolve(psModule); | |
442 | } | |
443 | ||
444 | return retry(error); | |
445 | }); | |
446 | }, { | |
447 | retries: 9, | |
448 | factor: 1, | |
449 | minTimeout: 333, | |
450 | maxTimeout: 333 | |
451 | }); | |
452 | } | |
453 | ||
454 | function match(regex, str) { | |
455 | var matches = str.match(regex); | |
456 | return matches && matches[1]; | |
457 | } | |
458 | ||
459 | function dargs(obj) { | |
460 | return Object.keys(obj).reduce(function (args, key) { | |
461 | var arg = '--' + key.replace(/[A-Z]/g, '-$&').toLowerCase(); | |
462 | var val = obj[key]; | |
463 | ||
464 | if (key === '_') val.forEach(function (v) { | |
465 | return args.push(v); | |
466 | });else if (Array.isArray(val)) val.forEach(function (v) { | |
467 | return args.push(arg, v); | |
468 | });else args.push(arg, obj[key]); | |
469 | ||
470 | return args; | |
471 | }, []); | |
472 | } |