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')
13 const ffiModuleRegex
= /\/\/\s+module\s+([\w\.]+)/i
14 const srcModuleRegex
= /(?:^|\n)module\s+([\w\.]+)/i
15 const requireRegex
= /require\(['"]\.\.\/([\w\.]+)['"]\)/g
17 module
.exports
= function purescriptLoader(source
, map
) {
18 const callback
= this.async()
19 const config
= this.options
20 const query
= loaderUtils
.parseQuery(this.query
)
21 const webpackOptions
= this.options
.purescriptLoader
|| {}
23 const options
= Object
.assign({
24 context: config
.context
,
27 pscBundle: 'psc-bundle',
30 pscIdeColors: webpackOptions
.psc
=== 'psa' || query
.psc
=== 'psa',
32 bundleOutput: 'output/bundle.js',
33 bundleNamespace: 'PS',
38 path
.join('src', '**', '*.purs'),
39 path
.join('bower_components', 'purescript-*', 'src', '**', '*.purs')
42 path
.join('src', '**', '*.js'),
43 path
.join('bower_components', 'purescript-*', 'src', '**', '*.js')
45 }, webpackOptions
, query
)
47 this.cacheable
&& this.cacheable()
49 let cache
= config
.purescriptLoaderCache
= config
.purescriptLoaderCache
|| {
55 if (!config
.purescriptLoaderInstalled
) {
56 config
.purescriptLoaderInstalled
= true
58 // invalidate loader cache when bundle is marked as invalid (in watch mode)
59 this._compiler
.plugin('invalid', () => {
60 cache
= config
.purescriptLoaderCache
= {
61 rebuild: options
.pscIde
,
63 ideServer: cache
.ideServer
67 // add psc warnings to webpack compilation warnings
68 this._compiler
.plugin('after-compile', (compilation
, callback
) => {
69 if (options
.warnings
&& cache
.warnings
) {
70 compilation
.warnings
.unshift(`PureScript compilation:\n${cache.warnings}`)
74 compilation
.errors
.unshift(`PureScript compilation:\n${cache.errors}`)
81 const psModuleName
= match(srcModuleRegex
, source
)
84 load: js
=> callback(null, js
),
85 reject: error
=> callback(error
),
86 srcPath: this.resourcePath
,
87 srcDir: path
.dirname(this.resourcePath
),
88 jsPath: path
.resolve(path
.join(options
.output
, psModuleName
, 'index.js')),
93 debug('loader called', psModule
.name
)
96 cache
.bundleModules
.push(psModule
.name
)
100 return connectIdeServer(psModule
)
104 .catch(psModule
.reject
)
107 if (cache
.compilationFinished
) {
108 return toJavaScript(psModule
).then(psModule
.load
).catch(psModule
.reject
)
111 // We need to wait for compilation to finish before the loaders run so that
112 // references to compiled output are valid.
113 cache
.deferred
.push(psModule
)
115 if (!cache
.compilationStarted
) {
116 return compile(psModule
)
117 .then(() => Promise
.map(cache
.deferred
, psModule
=> {
118 if (typeof cache
.ideServer
=== 'object') cache
.ideServer
.kill()
119 return toJavaScript(psModule
).then(psModule
.load
)
122 cache
.deferred
[0].reject(error
)
123 cache
.deferred
.slice(1).forEach(psModule
=> psModule
.reject(true))
128 // The actual loader is executed *after* purescript compilation.
129 function toJavaScript(psModule
) {
130 const options
= psModule
.options
131 const cache
= psModule
.cache
132 const bundlePath
= path
.resolve(options
.bundleOutput
)
133 const jsPath
= cache
.bundle
? bundlePath : psModule
.jsPath
135 debug('loading JavaScript for', psModule
.name
)
137 return Promise
.props({
138 js: fs
.readFileAsync(jsPath
, 'utf8'),
139 psModuleMap: psModuleMap(options
, cache
)
143 if (options
.bundle
) {
144 // if bundling, return a reference to the bundle
145 js
= 'module.exports = require("'
146 + path
.relative(psModule
.srcDir
, options
.bundleOutput
)
147 + '")["' + psModule
.name
+ '"]'
149 // replace require paths to output files generated by psc with paths
150 // to purescript sources, which are then also run through this loader.
152 .replace(requireRegex
, (m
, p1
) => {
153 return 'require("' + result
.psModuleMap
[p1
].src
+ '")'
155 .replace(/require\(['"]\.\/foreign['"]\)/g, (m
, p1
) => {
156 return 'require("' + result
.psModuleMap
[psModule
.name
].ffi
+ '")'
164 function compile(psModule
) {
165 const options
= psModule
.options
166 const cache
= psModule
.cache
169 if (cache
.compilationStarted
) return Promise
.resolve(psModule
)
171 cache
.compilationStarted
= true
173 const args
= dargs(Object
.assign({
176 output: options
.output
,
179 debug('spawning compiler %s %o', options
.psc
, args
)
181 return (new Promise((resolve
, reject
) => {
182 console
.log('\nCompiling PureScript...')
184 const compilation
= spawn(options
.psc
, args
)
186 compilation
.stdout
.on('data', data
=> stderr
.push(data
.toString()))
187 compilation
.stderr
.on('data', data
=> stderr
.push(data
.toString()))
189 compilation
.on('close', code
=> {
190 console
.log('Finished compiling PureScript.')
191 cache
.compilationFinished
= true
193 cache
.errors
= stderr
.join('')
196 cache
.warnings
= stderr
.join('')
201 .then(compilerOutput
=> {
202 if (options
.bundle
) {
203 return bundle(options
, cache
).then(() => psModule
)
209 function rebuild(psModule
) {
210 const options
= psModule
.options
211 const cache
= psModule
.cache
213 debug('attempting rebuild with psc-ide-client %s', psModule
.srcPath
)
215 const request
= (body
) => new Promise((resolve
, reject
) => {
216 const args
= dargs(options
.pscIdeArgs
)
217 const ideClient
= spawn('psc-ide-client', args
)
222 ideClient
.stdout
.on('data', data
=> {
223 stdout
= stdout
+ data
.toString()
226 ideClient
.stderr
.on('data', data
=> {
227 stderr
= stderr
+ data
.toString()
230 ideClient
.on('close', code
=> {
232 const error
= stderr
=== '' ? 'Failed to spawn psc-ide-client' : stderr
233 return reject(new Error(error
))
239 res
= JSON
.parse(stdout
.toString())
245 if (res
&& !Array
.isArray(res
.result
)) {
246 return res
.resultType
=== 'success'
248 : reject('psc-ide rebuild failed')
251 Promise
.map(res
.result
, (item
, i
) => {
253 return formatIdeResult(item
, options
, i
, res
.result
.length
)
255 .then(compileMessages
=> {
256 if (res
.resultType
=== 'error') {
257 if (res
.result
.some(item
=> item
.errorCode
=== 'UnknownModule')) {
258 console
.log('Unknown module, attempting full recompile')
259 return compile(psModule
)
260 .then(() => request({ command: 'load' }))
262 .catch(() => reject('psc-ide rebuild failed'))
264 cache
.errors
= compileMessages
.join('\n')
265 reject('psc-ide rebuild failed')
267 cache
.warnings
= compileMessages
.join('\n')
273 ideClient
.stdin
.write(JSON
.stringify(body
))
274 ideClient
.stdin
.write('\n')
280 file: psModule
.srcPath
,
285 function formatIdeResult(result
, options
, index
, length
) {
286 const srcPath
= path
.relative(options
.context
, result
.filename
)
287 const pos
= result
.position
288 const fileAndPos
= `${srcPath}:${pos.startLine}:${pos.startColumn}`
289 let numAndErr
= `[${index+1}/${length} ${result.errorCode}]`
290 numAndErr
= options
.pscIdeColors
? colors
.yellow(numAndErr
) : numAndErr
292 return fs
.readFileAsync(result
.filename
, 'utf8').then(source
=> {
293 const lines
= source
.split('\n').slice(pos
.startLine
- 1, pos
.endLine
)
294 const endsOnNewline
= pos
.endColumn
=== 1 && pos
.startLine
!== pos
.endLine
295 const up
= options
.pscIdeColors
? colors
.red('^') : '^'
296 const down
= options
.pscIdeColors
? colors
.red('v') : 'v'
297 let trimmed
= lines
.slice(0)
300 lines
.splice(lines
.length
- 1, 1)
301 pos
.endLine
= pos
.endLine
- 1
302 pos
.endColumn
= lines
[lines
.length
- 1].length
|| 1
305 // strip newlines at the end
307 trimmed
= lines
.reverse().reduce((trimmed
, line
, i
) => {
308 if (i
=== 0 && line
=== '') trimmed
.trimming
= true
309 if (!trimmed
.trimming
) trimmed
.push(line
)
310 if (trimmed
.trimming
&& line
!== '') {
311 trimmed
.trimming
= false
316 pos
.endLine
= pos
.endLine
- (lines
.length
- trimmed
.length
)
317 pos
.endColumn
= trimmed
[trimmed
.length
- 1].length
|| 1
320 const spaces
= ' '.repeat(String(pos
.endLine
).length
)
321 let snippet
= trimmed
.map((line
, i
) => {
322 return ` ${pos.startLine + i} ${line}`
325 if (trimmed
.length
=== 1) {
326 snippet
+= `\n ${spaces} ${' '.repeat(pos.startColumn - 1)}${up.repeat(pos.endColumn - pos.startColumn + 1)}`
328 snippet
= ` ${spaces} ${' '.repeat(pos.startColumn - 1)}${down}\n${snippet}`
329 snippet
+= `\n ${spaces} ${' '.repeat(pos.endColumn - 1)}${up}`
332 return Promise
.resolve(
333 `\n${numAndErr} ${fileAndPos}\n\n${snippet}\n\n${result.message}`
338 function bundle(options
, cache
) {
339 if (cache
.bundle
) return Promise
.resolve(cache
.bundle
)
342 const stderr
= cache
.bundle
= []
344 const args
= dargs(Object
.assign({
345 _: [path
.join(options
.output
, '*', '*.js')],
346 output: options
.bundleOutput
,
347 namespace: options
.bundleNamespace
,
348 }, options
.pscBundleArgs
))
350 cache
.bundleModules
.forEach(name
=> args
.push('--module', name
))
352 debug('spawning bundler %s %o', options
.pscBundle
, args
.join(' '))
354 return (new Promise((resolve
, reject
) => {
355 console
.log('Bundling PureScript...')
357 const compilation
= spawn(options
.pscBundle
, args
)
359 compilation
.stdout
.on('data', data
=> stdout
.push(data
.toString()))
360 compilation
.stderr
.on('data', data
=> stderr
.push(data
.toString()))
361 compilation
.on('close', code
=> {
363 cache
.errors
= (cache
.errors
|| '') + stderr
.join('')
366 cache
.bundle
= stderr
367 resolve(fs
.appendFileAsync('output/bundle.js', `module.exports = ${options.bundleNamespace}`))
372 // map of PS module names to their source path
373 function psModuleMap(options
, cache
) {
374 if (cache
.psModuleMap
) return Promise
.resolve(cache
.psModuleMap
)
376 const globs
= [].concat(options
.src
).concat(options
.ffi
)
378 return globby(globs
).then(paths
=> {
380 .props(paths
.reduce((map
, file
) => {
381 map
[file
] = fs
.readFileAsync(file
, 'utf8')
385 cache
.psModuleMap
= Object
.keys(fileMap
).reduce((map
, file
) => {
386 const source
= fileMap
[file
]
387 const ext
= path
.extname(file
)
388 const isPurs
= ext
.match(/purs$/i)
389 const moduleRegex
= isPurs
? srcModuleRegex : ffiModuleRegex
390 const moduleName
= match(moduleRegex
, source
)
391 map
[moduleName
] = map
[moduleName
] || {}
393 map
[moduleName
].src
= path
.resolve(file
)
395 map
[moduleName
].ffi
= path
.resolve(file
)
399 return cache
.psModuleMap
404 function connectIdeServer(psModule
) {
405 const options
= psModule
.options
406 const cache
= psModule
.cache
408 if (cache
.ideServer
) return Promise
.resolve(psModule
)
410 cache
.ideServer
= true
412 const connect
= () => new Promise((resolve
, reject
) => {
413 const args
= dargs(options
.pscIdeArgs
)
415 debug('attempting to connect to psc-ide-server', args
)
417 const ideClient
= spawn('psc-ide-client', args
)
419 ideClient
.stderr
.on('data', data
=> {
420 debug(data
.toString())
421 cache
.ideServer
= false
424 ideClient
.stdout
.once('data', data
=> {
425 debug(data
.toString())
426 if (data
.toString()[0] === '{') {
427 const res
= JSON
.parse(data
.toString())
428 if (res
.resultType
=== 'success') {
429 cache
.ideServer
= ideServer
432 cache
.ideServer
= ideServer
436 cache
.ideServer
= false
440 ideClient
.stdin
.resume()
441 ideClient
.stdin
.write(JSON
.stringify({ command: 'load' }))
442 ideClient
.stdin
.write('\n')
445 const args
= dargs(Object
.assign({
446 outputDirectory: options
.output
,
447 }, options
.pscIdeArgs
))
449 debug('attempting to start psc-ide-server', args
)
451 const ideServer
= cache
.ideServer
= spawn('psc-ide-server', [])
452 ideServer
.stderr
.on('data', data
=> {
453 debug(data
.toString())
456 return retryPromise((retry
, number
) => {
457 return connect().catch(error
=> {
458 if (!cache
.ideServer
&& number
=== 9) {
462 'failed to connect to or start psc-ide-server, ' +
463 'full compilation will occur on rebuild'
466 return Promise
.resolve(psModule
)
479 function match(regex
, str
) {
480 const matches
= str
.match(regex
)
481 return matches
&& matches
[1]
484 function dargs(obj
) {
485 return Object
.keys(obj
).reduce((args
, key
) => {
486 const arg
= '--' + key
.replace(/[A-Z]/g, '-$&').toLowerCase();
489 if (key
=== '_') val
.forEach(v
=> args
.push(v
))
490 else if (Array
.isArray(val
)) val
.forEach(v
=> args
.push(arg
, v
))
491 else args
.push(arg
, obj
[key
])
493 return args
.filter(arg
=> (typeof arg
!== 'boolean'))