3 const colors
= require('chalk')
4 const debug
= require('debug')('purs-loader')
5 const loaderUtils
= require('loader-utils')
6 const globby
= require('globby')
7 const Promise
= require('bluebird')
8 const fs
= Promise
.promisifyAll(require('fs'))
9 const spawn
= require('cross-spawn')
10 const path
= require('path')
11 const retryPromise
= require('promise-retry')
12 const jsStringEscape
= require('js-string-escape')
14 const ffiModuleRegex
= /\/\/\s+module\s+([\w\.]+)/i
15 const srcModuleRegex
= /(?:^|\n)module\s+([\w\.]+)/i
16 const requireRegex
= /require\(['"]\.\.\/([\w\.]+)['"]\)/g
18 module
.exports
= function purescriptLoader(source
, map
) {
19 const callback
= this.async()
20 const config
= this.options
21 const query
= loaderUtils
.parseQuery(this.query
)
22 const webpackOptions
= this.options
.purescriptLoader
|| {}
24 const options
= Object
.assign({
25 context: config
.context
,
28 pscBundle: 'psc-bundle',
31 pscIdeColors: webpackOptions
.psc
=== 'psa' || query
.psc
=== 'psa',
33 bundleOutput: 'output/bundle.js',
34 bundleNamespace: 'PS',
39 path
.join('src', '**', '*.purs'),
40 path
.join('bower_components', 'purescript-*', 'src', '**', '*.purs')
43 path
.join('src', '**', '*.js'),
44 path
.join('bower_components', 'purescript-*', 'src', '**', '*.js')
46 }, webpackOptions
, query
)
48 this.cacheable
&& this.cacheable()
50 let cache
= config
.purescriptLoaderCache
= config
.purescriptLoaderCache
|| {
56 if (!config
.purescriptLoaderInstalled
) {
57 config
.purescriptLoaderInstalled
= true
59 // invalidate loader cache when bundle is marked as invalid (in watch mode)
60 this._compiler
.plugin('invalid', () => {
61 cache
= config
.purescriptLoaderCache
= {
62 rebuild: options
.pscIde
,
64 ideServer: cache
.ideServer
68 // add psc warnings to webpack compilation warnings
69 this._compiler
.plugin('after-compile', (compilation
, callback
) => {
70 if (options
.warnings
&& cache
.warnings
) {
71 compilation
.warnings
.unshift(`PureScript compilation:\n${cache.warnings}`)
75 compilation
.errors
.unshift(`PureScript compilation:\n${cache.errors}`)
82 const psModuleName
= match(srcModuleRegex
, source
)
85 load: js
=> callback(null, js
),
86 reject: error
=> callback(error
),
87 srcPath: this.resourcePath
,
88 srcDir: path
.dirname(this.resourcePath
),
89 jsPath: path
.resolve(path
.join(options
.output
, psModuleName
, 'index.js')),
94 debug('loader called', psModule
.name
)
97 cache
.bundleModules
.push(psModule
.name
)
101 return connectIdeServer(psModule
)
105 .catch(psModule
.reject
)
108 if (cache
.compilationFinished
) {
109 return toJavaScript(psModule
).then(psModule
.load
).catch(psModule
.reject
)
112 // We need to wait for compilation to finish before the loaders run so that
113 // references to compiled output are valid.
114 cache
.deferred
.push(psModule
)
116 if (!cache
.compilationStarted
) {
117 return compile(psModule
)
118 .then(() => Promise
.map(cache
.deferred
, psModule
=> {
119 if (typeof cache
.ideServer
=== 'object') cache
.ideServer
.kill()
120 return toJavaScript(psModule
).then(psModule
.load
)
123 cache
.deferred
[0].reject(error
)
124 cache
.deferred
.slice(1).forEach(psModule
=> psModule
.reject(true))
129 // The actual loader is executed *after* purescript compilation.
130 function toJavaScript(psModule
) {
131 const options
= psModule
.options
132 const cache
= psModule
.cache
133 const bundlePath
= path
.resolve(options
.bundleOutput
)
134 const jsPath
= cache
.bundle
? bundlePath : psModule
.jsPath
136 debug('loading JavaScript for', psModule
.name
)
138 return Promise
.props({
139 js: fs
.readFileAsync(jsPath
, 'utf8'),
140 psModuleMap: psModuleMap(options
, cache
)
144 if (options
.bundle
) {
145 // if bundling, return a reference to the bundle
146 js
= 'module.exports = require("'
147 + jsStringEscape(path
.relative(psModule
.srcDir
, options
.bundleOutput
))
148 + '")["' + psModule
.name
+ '"]'
150 // replace require paths to output files generated by psc with paths
151 // to purescript sources, which are then also run through this loader.
153 .replace(requireRegex
, (m
, p1
) => {
154 return 'require("' + jsStringEscape(result
.psModuleMap
[p1
].src
) + '")'
156 .replace(/require\(['"]\.\/foreign['"]\)/g, (m
, p1
) => {
157 return 'require("' + jsStringEscape(result
.psModuleMap
[psModule
.name
].ffi
) + '")'
165 function compile(psModule
) {
166 const options
= psModule
.options
167 const cache
= psModule
.cache
170 if (cache
.compilationStarted
) return Promise
.resolve(psModule
)
172 cache
.compilationStarted
= true
174 const args
= dargs(Object
.assign({
177 output: options
.output
,
180 debug('spawning compiler %s %o', options
.psc
, args
)
182 return (new Promise((resolve
, reject
) => {
183 console
.log('\nCompiling PureScript...')
185 const compilation
= spawn(options
.psc
, args
)
187 compilation
.stdout
.on('data', data
=> stderr
.push(data
.toString()))
188 compilation
.stderr
.on('data', data
=> stderr
.push(data
.toString()))
190 compilation
.on('close', code
=> {
191 console
.log('Finished compiling PureScript.')
192 cache
.compilationFinished
= true
194 cache
.errors
= stderr
.join('')
197 cache
.warnings
= stderr
.join('')
202 .then(compilerOutput
=> {
203 if (options
.bundle
) {
204 return bundle(options
, cache
).then(() => psModule
)
210 function rebuild(psModule
) {
211 const options
= psModule
.options
212 const cache
= psModule
.cache
214 debug('attempting rebuild with psc-ide-client %s', psModule
.srcPath
)
216 const request
= (body
) => new Promise((resolve
, reject
) => {
217 const args
= dargs(options
.pscIdeArgs
)
218 const ideClient
= spawn('psc-ide-client', args
)
223 ideClient
.stdout
.on('data', data
=> {
224 stdout
= stdout
+ data
.toString()
227 ideClient
.stderr
.on('data', data
=> {
228 stderr
= stderr
+ data
.toString()
231 ideClient
.on('close', code
=> {
233 const error
= stderr
=== '' ? 'Failed to spawn psc-ide-client' : stderr
234 return reject(new Error(error
))
240 res
= JSON
.parse(stdout
.toString())
246 if (res
&& !Array
.isArray(res
.result
)) {
247 return res
.resultType
=== 'success'
249 : reject('psc-ide rebuild failed')
252 Promise
.map(res
.result
, (item
, i
) => {
254 return formatIdeResult(item
, options
, i
, res
.result
.length
)
256 .then(compileMessages
=> {
257 if (res
.resultType
=== 'error') {
258 if (res
.result
.some(item
=> item
.errorCode
=== 'UnknownModule')) {
259 console
.log('Unknown module, attempting full recompile')
260 return compile(psModule
)
261 .then(() => request({ command: 'load' }))
263 .catch(() => reject('psc-ide rebuild failed'))
265 cache
.errors
= compileMessages
.join('\n')
266 reject('psc-ide rebuild failed')
268 cache
.warnings
= compileMessages
.join('\n')
274 ideClient
.stdin
.write(JSON
.stringify(body
))
275 ideClient
.stdin
.write('\n')
281 file: psModule
.srcPath
,
286 function formatIdeResult(result
, options
, index
, length
) {
287 const srcPath
= path
.relative(options
.context
, result
.filename
)
288 const pos
= result
.position
289 const fileAndPos
= `${srcPath}:${pos.startLine}:${pos.startColumn}`
290 let numAndErr
= `[${index+1}/${length} ${result.errorCode}]`
291 numAndErr
= options
.pscIdeColors
? colors
.yellow(numAndErr
) : numAndErr
293 return fs
.readFileAsync(result
.filename
, 'utf8').then(source
=> {
294 const lines
= source
.split('\n').slice(pos
.startLine
- 1, pos
.endLine
)
295 const endsOnNewline
= pos
.endColumn
=== 1 && pos
.startLine
!== pos
.endLine
296 const up
= options
.pscIdeColors
? colors
.red('^') : '^'
297 const down
= options
.pscIdeColors
? colors
.red('v') : 'v'
298 let trimmed
= lines
.slice(0)
301 lines
.splice(lines
.length
- 1, 1)
302 pos
.endLine
= pos
.endLine
- 1
303 pos
.endColumn
= lines
[lines
.length
- 1].length
|| 1
306 // strip newlines at the end
308 trimmed
= lines
.reverse().reduce((trimmed
, line
, i
) => {
309 if (i
=== 0 && line
=== '') trimmed
.trimming
= true
310 if (!trimmed
.trimming
) trimmed
.push(line
)
311 if (trimmed
.trimming
&& line
!== '') {
312 trimmed
.trimming
= false
317 pos
.endLine
= pos
.endLine
- (lines
.length
- trimmed
.length
)
318 pos
.endColumn
= trimmed
[trimmed
.length
- 1].length
|| 1
321 const spaces
= ' '.repeat(String(pos
.endLine
).length
)
322 let snippet
= trimmed
.map((line
, i
) => {
323 return ` ${pos.startLine + i} ${line}`
326 if (trimmed
.length
=== 1) {
327 snippet
+= `\n ${spaces} ${' '.repeat(pos.startColumn - 1)}${up.repeat(pos.endColumn - pos.startColumn + 1)}`
329 snippet
= ` ${spaces} ${' '.repeat(pos.startColumn - 1)}${down}\n${snippet}`
330 snippet
+= `\n ${spaces} ${' '.repeat(pos.endColumn - 1)}${up}`
333 return Promise
.resolve(
334 `\n${numAndErr} ${fileAndPos}\n\n${snippet}\n\n${result.message}`
339 function bundle(options
, cache
) {
340 if (cache
.bundle
) return Promise
.resolve(cache
.bundle
)
343 const stderr
= cache
.bundle
= []
345 const args
= dargs(Object
.assign({
346 _: [path
.join(options
.output
, '*', '*.js')],
347 output: options
.bundleOutput
,
348 namespace: options
.bundleNamespace
,
349 }, options
.pscBundleArgs
))
351 cache
.bundleModules
.forEach(name
=> args
.push('--module', name
))
353 debug('spawning bundler %s %o', options
.pscBundle
, args
.join(' '))
355 return (new Promise((resolve
, reject
) => {
356 console
.log('Bundling PureScript...')
358 const compilation
= spawn(options
.pscBundle
, args
)
360 compilation
.stdout
.on('data', data
=> stdout
.push(data
.toString()))
361 compilation
.stderr
.on('data', data
=> stderr
.push(data
.toString()))
362 compilation
.on('close', code
=> {
364 cache
.errors
= (cache
.errors
|| '') + stderr
.join('')
367 cache
.bundle
= stderr
368 resolve(fs
.appendFileAsync('output/bundle.js', `module.exports = ${options.bundleNamespace}`))
373 // map of PS module names to their source path
374 function psModuleMap(options
, cache
) {
375 if (cache
.psModuleMap
) return Promise
.resolve(cache
.psModuleMap
)
377 const globs
= [].concat(options
.src
).concat(options
.ffi
)
379 return globby(globs
).then(paths
=> {
381 .props(paths
.reduce((map
, file
) => {
382 map
[file
] = fs
.readFileAsync(file
, 'utf8')
386 cache
.psModuleMap
= Object
.keys(fileMap
).reduce((map
, file
) => {
387 const source
= fileMap
[file
]
388 const ext
= path
.extname(file
)
389 const isPurs
= ext
.match(/purs$/i)
390 const moduleRegex
= isPurs
? srcModuleRegex : ffiModuleRegex
391 const moduleName
= match(moduleRegex
, source
)
392 map
[moduleName
] = map
[moduleName
] || {}
394 map
[moduleName
].src
= path
.resolve(file
)
396 map
[moduleName
].ffi
= path
.resolve(file
)
400 return cache
.psModuleMap
405 function connectIdeServer(psModule
) {
406 const options
= psModule
.options
407 const cache
= psModule
.cache
409 if (cache
.ideServer
) return Promise
.resolve(psModule
)
411 cache
.ideServer
= true
413 const connect
= () => new Promise((resolve
, reject
) => {
414 const args
= dargs(options
.pscIdeArgs
)
416 debug('attempting to connect to psc-ide-server', args
)
418 const ideClient
= spawn('psc-ide-client', args
)
420 ideClient
.stderr
.on('data', data
=> {
421 debug(data
.toString())
422 cache
.ideServer
= false
425 ideClient
.stdout
.once('data', data
=> {
426 debug(data
.toString())
427 if (data
.toString()[0] === '{') {
428 const res
= JSON
.parse(data
.toString())
429 if (res
.resultType
=== 'success') {
430 cache
.ideServer
= ideServer
433 cache
.ideServer
= ideServer
437 cache
.ideServer
= false
441 ideClient
.stdin
.resume()
442 ideClient
.stdin
.write(JSON
.stringify({ command: 'load' }))
443 ideClient
.stdin
.write('\n')
446 const args
= dargs(Object
.assign({
447 outputDirectory: options
.output
,
448 }, options
.pscIdeArgs
))
450 debug('attempting to start psc-ide-server', args
)
452 const ideServer
= cache
.ideServer
= spawn('psc-ide-server', [])
453 ideServer
.stderr
.on('data', data
=> {
454 debug(data
.toString())
457 return retryPromise((retry
, number
) => {
458 return connect().catch(error
=> {
459 if (!cache
.ideServer
&& number
=== 9) {
463 'failed to connect to or start psc-ide-server, ' +
464 'full compilation will occur on rebuild'
467 return Promise
.resolve(psModule
)
480 function match(regex
, str
) {
481 const matches
= str
.match(regex
)
482 return matches
&& matches
[1]
485 function dargs(obj
) {
486 return Object
.keys(obj
).reduce((args
, key
) => {
487 const arg
= '--' + key
.replace(/[A-Z]/g, '-$&').toLowerCase();
490 if (key
=== '_') val
.forEach(v
=> args
.push(v
))
491 else if (Array
.isArray(val
)) val
.forEach(v
=> args
.push(arg
, v
))
492 else args
.push(arg
, obj
[key
])
494 return args
.filter(arg
=> (typeof arg
!== 'boolean'))