aboutsummaryrefslogtreecommitdiffhomepage
path: root/src/index.js
diff options
context:
space:
mode:
Diffstat (limited to 'src/index.js')
-rw-r--r--src/index.js394
1 files changed, 39 insertions, 355 deletions
diff --git a/src/index.js b/src/index.js
index cfba1e2..fe1455b 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,17 +1,16 @@
1'use strict' 1'use strict'
2 2
3const colors = require('chalk')
4const debug = require('debug')('purs-loader') 3const debug = require('debug')('purs-loader')
5const loaderUtils = require('loader-utils') 4const loaderUtils = require('loader-utils')
6const globby = require('globby')
7const Promise = require('bluebird') 5const Promise = require('bluebird')
8const fs = Promise.promisifyAll(require('fs')) 6const fs = Promise.promisifyAll(require('fs'))
9const spawn = require('cross-spawn')
10const path = require('path') 7const path = require('path')
11const retryPromise = require('promise-retry')
12const jsStringEscape = require('js-string-escape') 8const jsStringEscape = require('js-string-escape')
9const PsModuleMap = require('./PsModuleMap');
10const Psc = require('./Psc');
11const PscIde = require('./PscIde');
12const dargs = require('./dargs');
13 13
14const srcModuleRegex = /(?:^|\n)module\s+([\w\.]+)/i
15const requireRegex = /require\(['"]\.\.\/([\w\.]+)['"]\)/g 14const requireRegex = /require\(['"]\.\.\/([\w\.]+)['"]\)/g
16 15
17module.exports = function purescriptLoader(source, map) { 16module.exports = function purescriptLoader(source, map) {
@@ -45,7 +44,7 @@ module.exports = function purescriptLoader(source, map) {
45 let cache = config.purescriptLoaderCache = config.purescriptLoaderCache || { 44 let cache = config.purescriptLoaderCache = config.purescriptLoaderCache || {
46 rebuild: false, 45 rebuild: false,
47 deferred: [], 46 deferred: [],
48 bundleModules: [], 47 bundleModules: []
49 } 48 }
50 49
51 if (!config.purescriptLoaderInstalled) { 50 if (!config.purescriptLoaderInstalled) {
@@ -53,10 +52,14 @@ module.exports = function purescriptLoader(source, map) {
53 52
54 // invalidate loader cache when bundle is marked as invalid (in watch mode) 53 // invalidate loader cache when bundle is marked as invalid (in watch mode)
55 this._compiler.plugin('invalid', () => { 54 this._compiler.plugin('invalid', () => {
55 debug('invalidating loader cache');
56
56 cache = config.purescriptLoaderCache = { 57 cache = config.purescriptLoaderCache = {
57 rebuild: options.pscIde, 58 rebuild: options.pscIde,
58 deferred: [], 59 deferred: [],
59 ideServer: cache.ideServer 60 bundleModules: [],
61 ideServer: cache.ideServer,
62 psModuleMap: cache.psModuleMap
60 } 63 }
61 }) 64 })
62 65
@@ -74,7 +77,7 @@ module.exports = function purescriptLoader(source, map) {
74 }) 77 })
75 } 78 }
76 79
77 const psModuleName = match(srcModuleRegex, source) 80 const psModuleName = PsModuleMap.match(source)
78 const psModule = { 81 const psModule = {
79 name: psModuleName, 82 name: psModuleName,
80 load: js => callback(null, js), 83 load: js => callback(null, js),
@@ -93,8 +96,8 @@ module.exports = function purescriptLoader(source, map) {
93 } 96 }
94 97
95 if (cache.rebuild) { 98 if (cache.rebuild) {
96 return connectIdeServer(psModule) 99 return PscIde.connect(psModule)
97 .then(rebuild) 100 .then(PscIde.rebuild)
98 .then(toJavaScript) 101 .then(toJavaScript)
99 .then(psModule.load) 102 .then(psModule.load)
100 .catch(psModule.reject) 103 .catch(psModule.reject)
@@ -109,7 +112,11 @@ module.exports = function purescriptLoader(source, map) {
109 cache.deferred.push(psModule) 112 cache.deferred.push(psModule)
110 113
111 if (!cache.compilationStarted) { 114 if (!cache.compilationStarted) {
112 return compile(psModule) 115 return Psc.compile(psModule)
116 .then(() => PsModuleMap.makeMap(options.src).then(map => {
117 debug('rebuilt module map');
118 cache.psModuleMap = map;
119 }))
113 .then(() => Promise.map(cache.deferred, psModule => { 120 .then(() => Promise.map(cache.deferred, psModule => {
114 if (typeof cache.ideServer === 'object') cache.ideServer.kill() 121 if (typeof cache.ideServer === 'object') cache.ideServer.kill()
115 return toJavaScript(psModule).then(psModule.load) 122 return toJavaScript(psModule).then(psModule.load)
@@ -121,6 +128,26 @@ module.exports = function purescriptLoader(source, map) {
121 } 128 }
122} 129}
123 130
131function updatePsModuleMap(psModule) {
132 const options = psModule.options
133 const cache = psModule.cache
134 const filePurs = psModule.srcPath
135 if (!cache.psModuleMap) {
136 debug('module mapping does not exist');
137 return PsModuleMap.makeMap(options.src).then(map => {
138 cache.psModuleMap = map;
139 return cache.psModuleMap;
140 });
141 }
142 else {
143 return PsModuleMap.makeMapEntry(filePurs).then(result => {
144 const map = Object.assign(cache.psModuleMap, result)
145 cache.psModuleMap = map;
146 return cache.psModuleMap;
147 });
148 }
149}
150
124// The actual loader is executed *after* purescript compilation. 151// The actual loader is executed *after* purescript compilation.
125function toJavaScript(psModule) { 152function toJavaScript(psModule) {
126 const options = psModule.options 153 const options = psModule.options
@@ -132,7 +159,7 @@ function toJavaScript(psModule) {
132 159
133 return Promise.props({ 160 return Promise.props({
134 js: fs.readFileAsync(jsPath, 'utf8'), 161 js: fs.readFileAsync(jsPath, 'utf8'),
135 psModuleMap: psModuleMap(options, cache) 162 psModuleMap: updatePsModuleMap(psModule)
136 }).then(result => { 163 }).then(result => {
137 let js = '' 164 let js = ''
138 165
@@ -156,346 +183,3 @@ function toJavaScript(psModule) {
156 return js 183 return js
157 }) 184 })
158} 185}
159
160function compile(psModule) {
161 const options = psModule.options
162 const cache = psModule.cache
163 const stderr = []
164
165 if (cache.compilationStarted) return Promise.resolve(psModule)
166
167 cache.compilationStarted = true
168
169 const args = dargs(Object.assign({
170 _: options.src,
171 output: options.output,
172 }, options.pscArgs))
173
174 debug('spawning compiler %s %o', options.psc, args)
175
176 return (new Promise((resolve, reject) => {
177 console.log('\nCompiling PureScript...')
178
179 const compilation = spawn(options.psc, args)
180
181 compilation.stdout.on('data', data => stderr.push(data.toString()))
182 compilation.stderr.on('data', data => stderr.push(data.toString()))
183
184 compilation.on('close', code => {
185 console.log('Finished compiling PureScript.')
186 cache.compilationFinished = true
187 if (code !== 0) {
188 cache.errors = stderr.join('')
189 reject(true)
190 } else {
191 cache.warnings = stderr.join('')
192 resolve(psModule)
193 }
194 })
195 }))
196 .then(compilerOutput => {
197 if (options.bundle) {
198 return bundle(options, cache).then(() => psModule)
199 }
200 return psModule
201 })
202}
203
204function rebuild(psModule) {
205 const options = psModule.options
206 const cache = psModule.cache
207
208 debug('attempting rebuild with psc-ide-client %s', psModule.srcPath)
209
210 const request = (body) => new Promise((resolve, reject) => {
211 const args = dargs(options.pscIdeArgs)
212 const ideClient = spawn('psc-ide-client', args)
213
214 var stdout = ''
215 var stderr = ''
216
217 ideClient.stdout.on('data', data => {
218 stdout = stdout + data.toString()
219 })
220
221 ideClient.stderr.on('data', data => {
222 stderr = stderr + data.toString()
223 })
224
225 ideClient.on('close', code => {
226 if (code !== 0) {
227 const error = stderr === '' ? 'Failed to spawn psc-ide-client' : stderr
228 return reject(new Error(error))
229 }
230
231 let res = null
232
233 try {
234 res = JSON.parse(stdout.toString())
235 debug(res)
236 } catch (err) {
237 return reject(err)
238 }
239
240 if (res && !Array.isArray(res.result)) {
241 return res.resultType === 'success'
242 ? resolve(psModule)
243 : reject('psc-ide rebuild failed')
244 }
245
246 Promise.map(res.result, (item, i) => {
247 debug(item)
248 return formatIdeResult(item, options, i, res.result.length)
249 })
250 .then(compileMessages => {
251 if (res.resultType === 'error') {
252 if (res.result.some(item => item.errorCode === 'UnknownModule')) {
253 console.log('Unknown module, attempting full recompile')
254 return compile(psModule)
255 .then(() => request({ command: 'load' }))
256 .then(resolve)
257 .catch(() => reject('psc-ide rebuild failed'))
258 }
259 cache.errors = compileMessages.join('\n')
260 reject('psc-ide rebuild failed')
261 } else {
262 cache.warnings = compileMessages.join('\n')
263 resolve(psModule)
264 }
265 })
266 })
267
268 ideClient.stdin.write(JSON.stringify(body))
269 ideClient.stdin.write('\n')
270 })
271
272 return request({
273 command: 'rebuild',
274 params: {
275 file: psModule.srcPath,
276 }
277 })
278}
279
280function formatIdeResult(result, options, index, length) {
281 const srcPath = path.relative(options.context, result.filename)
282 const pos = result.position
283 const fileAndPos = `${srcPath}:${pos.startLine}:${pos.startColumn}`
284 let numAndErr = `[${index+1}/${length} ${result.errorCode}]`
285 numAndErr = options.pscIdeColors ? colors.yellow(numAndErr) : numAndErr
286
287 return fs.readFileAsync(result.filename, 'utf8').then(source => {
288 const lines = source.split('\n').slice(pos.startLine - 1, pos.endLine)
289 const endsOnNewline = pos.endColumn === 1 && pos.startLine !== pos.endLine
290 const up = options.pscIdeColors ? colors.red('^') : '^'
291 const down = options.pscIdeColors ? colors.red('v') : 'v'
292 let trimmed = lines.slice(0)
293
294 if (endsOnNewline) {
295 lines.splice(lines.length - 1, 1)
296 pos.endLine = pos.endLine - 1
297 pos.endColumn = lines[lines.length - 1].length || 1
298 }
299
300 // strip newlines at the end
301 if (endsOnNewline) {
302 trimmed = lines.reverse().reduce((trimmed, line, i) => {
303 if (i === 0 && line === '') trimmed.trimming = true
304 if (!trimmed.trimming) trimmed.push(line)
305 if (trimmed.trimming && line !== '') {
306 trimmed.trimming = false
307 trimmed.push(line)
308 }
309 return trimmed
310 }, []).reverse()
311 pos.endLine = pos.endLine - (lines.length - trimmed.length)
312 pos.endColumn = trimmed[trimmed.length - 1].length || 1
313 }
314
315 const spaces = ' '.repeat(String(pos.endLine).length)
316 let snippet = trimmed.map((line, i) => {
317 return ` ${pos.startLine + i} ${line}`
318 }).join('\n')
319
320 if (trimmed.length === 1) {
321 snippet += `\n ${spaces} ${' '.repeat(pos.startColumn - 1)}${up.repeat(pos.endColumn - pos.startColumn + 1)}`
322 } else {
323 snippet = ` ${spaces} ${' '.repeat(pos.startColumn - 1)}${down}\n${snippet}`
324 snippet += `\n ${spaces} ${' '.repeat(pos.endColumn - 1)}${up}`
325 }
326
327 return Promise.resolve(
328 `\n${numAndErr} ${fileAndPos}\n\n${snippet}\n\n${result.message}`
329 )
330 })
331}
332
333function bundle(options, cache) {
334 if (cache.bundle) return Promise.resolve(cache.bundle)
335
336 const stdout = []
337 const stderr = cache.bundle = []
338
339 const args = dargs(Object.assign({
340 _: [path.join(options.output, '*', '*.js')],
341 output: options.bundleOutput,
342 namespace: options.bundleNamespace,
343 }, options.pscBundleArgs))
344
345 cache.bundleModules.forEach(name => args.push('--module', name))
346
347 debug('spawning bundler %s %o', options.pscBundle, args.join(' '))
348
349 return (new Promise((resolve, reject) => {
350 console.log('Bundling PureScript...')
351
352 const compilation = spawn(options.pscBundle, args)
353
354 compilation.stdout.on('data', data => stdout.push(data.toString()))
355 compilation.stderr.on('data', data => stderr.push(data.toString()))
356 compilation.on('close', code => {
357 if (code !== 0) {
358 cache.errors = (cache.errors || '') + stderr.join('')
359 return reject(true)
360 }
361 cache.bundle = stderr
362 resolve(fs.appendFileAsync('output/bundle.js', `module.exports = ${options.bundleNamespace}`))
363 })
364 }))
365}
366
367// map of PS module names to their source path
368function psModuleMap(options, cache) {
369 if (cache.psModuleMap) return Promise.resolve(cache.psModuleMap)
370
371 const globs = [].concat(options.src);
372
373 function pursToJs(file){
374 const dirname = path.dirname(file)
375 const basename = path.basename(file, '.purs')
376 const fileJS = path.join(dirname, `${basename}.js`)
377 return fileJS
378 }
379
380 return globby(globs).then(paths => {
381 return Promise
382 .props(paths.reduce((map, file) => {
383 const fileJS = pursToJs(file)
384 map[file] = fs.readFileAsync(file, 'utf8')
385 map[fileJS] = fs.readFileAsync(fileJS, 'utf8').catch(() => undefined)
386 return map
387 }, {}))
388 .then(fileMap => {
389 cache.psModuleMap = Object.keys(fileMap).reduce((map, file) => {
390 const ext = path.extname(file)
391 const isPurs = ext.match(/purs$/i)
392 if (isPurs) {
393 const fileJs = pursToJs(file)
394 const source = fileMap[file]
395 const ffi = fileMap[fileJs]
396 const moduleName = match(srcModuleRegex, source)
397 map[moduleName] = map[moduleName] || {}
398 map[moduleName].src = path.resolve(file)
399 if (ffi) {
400 map[moduleName].ffi = path.resolve(fileJs)
401 }
402 }
403 return map
404 }, {})
405 return cache.psModuleMap
406 })
407 })
408}
409
410function connectIdeServer(psModule) {
411 const options = psModule.options
412 const cache = psModule.cache
413
414 if (cache.ideServer) return Promise.resolve(psModule)
415
416 cache.ideServer = true
417
418 const connect = () => new Promise((resolve, reject) => {
419 const args = dargs(options.pscIdeArgs)
420
421 debug('attempting to connect to psc-ide-server', args)
422
423 const ideClient = spawn('psc-ide-client', args)
424
425 ideClient.stderr.on('data', data => {
426 debug(data.toString())
427 cache.ideServer = false
428 reject(true)
429 })
430 ideClient.stdout.once('data', data => {
431 debug(data.toString())
432 if (data.toString()[0] === '{') {
433 const res = JSON.parse(data.toString())
434 if (res.resultType === 'success') {
435 cache.ideServer = ideServer
436 resolve(psModule)
437 } else {
438 cache.ideServer = ideServer
439 reject(true)
440 }
441 } else {
442 cache.ideServer = false
443 reject(true)
444 }
445 })
446 ideClient.stdin.resume()
447 ideClient.stdin.write(JSON.stringify({ command: 'load' }))
448 ideClient.stdin.write('\n')
449 })
450
451 const args = dargs(Object.assign({
452 outputDirectory: options.output,
453 }, options.pscIdeArgs))
454
455 debug('attempting to start psc-ide-server', args)
456
457 const ideServer = cache.ideServer = spawn('psc-ide-server', [])
458 ideServer.stderr.on('data', data => {
459 debug(data.toString())
460 })
461
462 return retryPromise((retry, number) => {
463 return connect().catch(error => {
464 if (!cache.ideServer && number === 9) {
465 debug(error)
466
467 console.log(
468 'failed to connect to or start psc-ide-server, ' +
469 'full compilation will occur on rebuild'
470 )
471
472 return Promise.resolve(psModule)
473 }
474
475 return retry(error)
476 })
477 }, {
478 retries: 9,
479 factor: 1,
480 minTimeout: 333,
481 maxTimeout: 333,
482 })
483}
484
485function match(regex, str) {
486 const matches = str.match(regex)
487 return matches && matches[1]
488}
489
490function dargs(obj) {
491 return Object.keys(obj).reduce((args, key) => {
492 const arg = '--' + key.replace(/[A-Z]/g, '-$&').toLowerCase();
493 const val = obj[key]
494
495 if (key === '_') val.forEach(v => args.push(v))
496 else if (Array.isArray(val)) val.forEach(v => args.push(arg, v))
497 else args.push(arg, obj[key])
498
499 return args.filter(arg => (typeof arg !== 'boolean'))
500 }, [])
501}