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('child_process').spawn
10 const path
= require('path')
11 const retryPromise
= require('promise-retry')
13 const psModuleRegex
= /(?:^|\n)module\s+([\w\.]+)/i
14 const requireRegex
= /require\(['"]\.\.\/([\w\.]+)['"]\)/g
16 module
.exports
= function purescriptLoader(source
, map
) {
17 const callback
= this.async()
18 const config
= this.options
19 const query
= loaderUtils
.parseQuery(this.query
)
20 const webpackOptions
= this.options
.purescriptLoader
|| {}
22 const options
= Object
.assign({
23 context: config
.context
,
26 pscBundle: 'psc-bundle',
29 pscIdeColors: webpackOptions
.psc
=== 'psa' || query
.psc
=== 'psa',
31 bundleOutput: 'output/bundle.js',
32 bundleNamespace: 'PS',
37 path
.join('src', '**', '*.purs'),
38 path
.join('bower_components', 'purescript-*', 'src', '**', '*.purs')
41 path
.join('src', '**', '*.js'),
42 path
.join('bower_components', 'purescript-*', 'src', '**', '*.js')
44 }, webpackOptions
, query
)
46 this.cacheable
&& this.cacheable()
48 let cache
= config
.purescriptLoaderCache
= config
.purescriptLoaderCache
|| {
54 if (!config
.purescriptLoaderInstalled
) {
55 config
.purescriptLoaderInstalled
= true
57 // invalidate loader cache when bundle is marked as invalid (in watch mode)
58 this._compiler
.plugin('invalid', () => {
59 cache
= config
.purescriptLoaderCache
= {
60 rebuild: options
.pscIde
,
62 ideServer: cache
.ideServer
66 // add psc warnings to webpack compilation warnings
67 this._compiler
.plugin('after-compile', (compilation
, callback
) => {
68 if (options
.warnings
&& cache
.warnings
) {
69 compilation
.warnings
.unshift(`PureScript compilation:\n${cache.warnings}`)
73 compilation
.errors
.unshift(`PureScript compilation:\n${cache.errors}`)
80 const psModuleName
= match(psModuleRegex
, source
)
83 load: js
=> callback(null, js
),
84 reject: error
=> callback(error
),
85 srcPath: this.resourcePath
,
86 srcDir: path
.dirname(this.resourcePath
),
87 jsPath: path
.resolve(path
.join(options
.output
, psModuleName
, 'index.js')),
92 debug('loader called', psModule
.name
)
95 cache
.bundleModules
.push(psModule
.name
)
99 return connectIdeServer(psModule
)
103 .catch(psModule
.reject
)
106 if (cache
.compilationFinished
) {
107 return toJavaScript(psModule
).then(psModule
.load
).catch(psModule
.reject
)
110 // We need to wait for compilation to finish before the loaders run so that
111 // references to compiled output are valid.
112 cache
.deferred
.push(psModule
)
114 if (!cache
.compilationStarted
) {
115 return compile(psModule
)
116 .then(() => Promise
.map(cache
.deferred
, psModule
=> {
117 if (typeof cache
.ideServer
=== 'object') cache
.ideServer
.kill()
118 return toJavaScript(psModule
).then(psModule
.load
)
121 cache
.deferred
[0].reject(error
)
122 cache
.deferred
.slice(1).forEach(psModule
=> psModule
.reject(true))
127 // The actual loader is executed *after* purescript compilation.
128 function toJavaScript(psModule
) {
129 const options
= psModule
.options
130 const cache
= psModule
.cache
131 const bundlePath
= path
.resolve(options
.bundleOutput
)
132 const jsPath
= cache
.bundle
? bundlePath : psModule
.jsPath
134 debug('loading JavaScript for', psModule
.name
)
136 return Promise
.props({
137 js: fs
.readFileAsync(jsPath
, 'utf8'),
138 psModuleMap: psModuleMap(options
.src
, cache
)
142 if (options
.bundle
) {
143 // if bundling, return a reference to the bundle
144 js
= 'module.exports = require("'
145 + path
.relative(psModule
.srcDir
, options
.bundleOutput
)
146 + '")["' + psModule
.name
+ '"]'
148 // replace require paths to output files generated by psc with paths
149 // to purescript sources, which are then also run through this loader.
150 const foreignRequire
= 'require("' + path
.resolve(
151 path
.join(psModule
.options
.output
, psModule
.name
, 'foreign.js')
155 .replace(requireRegex
, (m
, p1
) => {
156 return 'require("' + result
.psModuleMap
[p1
] + '")'
158 .replace(/require\(['"]\.\/foreign['"]\)/g, foreignRequire
)
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
)
220 ideClient
.stdout
.once('data', data
=> {
224 res
= JSON
.parse(data
.toString())
230 if (res
&& !Array
.isArray(res
.result
)) {
231 return res
.resultType
=== 'success'
233 : reject('psc-ide rebuild failed')
236 Promise
.map(res
.result
, (item
, i
) => {
238 return formatIdeResult(item
, options
, i
, res
.result
.length
)
240 .then(compileMessages
=> {
241 if (res
.resultType
=== 'error') {
242 if (res
.result
.some(item
=> item
.errorCode
=== 'UnknownModule')) {
243 console
.log('Unknown module, attempting full recompile')
244 return compile(psModule
)
245 .then(() => request({ command: 'load' }))
247 .catch(() => reject('psc-ide rebuild failed'))
249 cache
.errors
= compileMessages
.join('\n')
250 reject('psc-ide rebuild failed')
252 cache
.warnings
= compileMessages
.join('\n')
258 ideClient
.stderr
.once('data', data
=> reject(data
.toString()))
260 ideClient
.stdin
.write(JSON
.stringify(body
))
261 ideClient
.stdin
.write('\n')
267 file: psModule
.srcPath
,
272 function formatIdeResult(result
, options
, index
, length
) {
273 const srcPath
= path
.relative(options
.context
, result
.filename
)
274 const pos
= result
.position
275 const fileAndPos
= `${srcPath}:${pos.startLine}:${pos.startColumn}`
276 let numAndErr
= `[${index+1}/${length} ${result.errorCode}]`
277 numAndErr
= options
.pscIdeColors
? colors
.yellow(numAndErr
) : numAndErr
279 return fs
.readFileAsync(result
.filename
, 'utf8').then(source
=> {
280 const lines
= source
.split('\n').slice(pos
.startLine
- 1, pos
.endLine
)
281 const endsOnNewline
= pos
.endColumn
=== 1 && pos
.startLine
!== pos
.endLine
282 const up
= options
.pscIdeColors
? colors
.red('^') : '^'
283 const down
= options
.pscIdeColors
? colors
.red('v') : 'v'
284 let trimmed
= lines
.slice(0)
287 lines
.splice(lines
.length
- 1, 1)
288 pos
.endLine
= pos
.endLine
- 1
289 pos
.endColumn
= lines
[lines
.length
- 1].length
|| 1
292 // strip newlines at the end
294 trimmed
= lines
.reverse().reduce((trimmed
, line
, i
) => {
295 if (i
=== 0 && line
=== '') trimmed
.trimming
= true
296 if (!trimmed
.trimming
) trimmed
.push(line
)
297 if (trimmed
.trimming
&& line
!== '') {
298 trimmed
.trimming
= false
303 pos
.endLine
= pos
.endLine
- (lines
.length
- trimmed
.length
)
304 pos
.endColumn
= trimmed
[trimmed
.length
- 1].length
|| 1
307 const spaces
= ' '.repeat(String(pos
.endLine
).length
)
308 let snippet
= trimmed
.map((line
, i
) => {
309 return ` ${pos.startLine + i} ${line}`
312 if (trimmed
.length
=== 1) {
313 snippet
+= `\n ${spaces} ${' '.repeat(pos.startColumn - 1)}${up.repeat(pos.endColumn - pos.startColumn + 1)}`
315 snippet
= ` ${spaces} ${' '.repeat(pos.startColumn - 1)}${down}\n${snippet}`
316 snippet
+= `\n ${spaces} ${' '.repeat(pos.endColumn - 1)}${up}`
319 return Promise
.resolve(
320 `\n${numAndErr} ${fileAndPos}\n\n${snippet}\n\n${result.message}`
325 function bundle(options
, cache
) {
326 if (cache
.bundle
) return Promise
.resolve(cache
.bundle
)
329 const stderr
= cache
.bundle
= []
331 const args
= dargs(Object
.assign({
332 _: [path
.join(options
.output
, '*', '*.js')],
333 output: options
.bundleOutput
,
334 namespace: options
.bundleNamespace
,
335 }, options
.pscBundleArgs
))
337 cache
.bundleModules
.forEach(name
=> args
.push('--module', name
))
339 debug('spawning bundler %s %o', options
.pscBundle
, args
.join(' '))
341 return (new Promise((resolve
, reject
) => {
342 console
.log('Bundling PureScript...')
344 const compilation
= spawn(options
.pscBundle
, args
)
346 compilation
.stdout
.on('data', data
=> stdout
.push(data
.toString()))
347 compilation
.stderr
.on('data', data
=> stderr
.push(data
.toString()))
348 compilation
.on('close', code
=> {
350 cache
.errors
= (cache
.errors
|| '') + stderr
.join('')
353 cache
.bundle
= stderr
354 resolve(fs
.appendFileAsync('output/bundle.js', `module.exports = ${options.bundleNamespace}`))
359 // map of PS module names to their source path
360 function psModuleMap(globs
, cache
) {
361 if (cache
.psModuleMap
) return Promise
.resolve(cache
.psModuleMap
)
363 return globby(globs
).then(paths
=> {
365 .props(paths
.reduce((map
, file
) => {
366 map
[file
] = fs
.readFileAsync(file
, 'utf8')
370 cache
.psModuleMap
= Object
.keys(srcMap
).reduce((map
, file
) => {
371 const source
= srcMap
[file
]
372 const psModuleName
= match(psModuleRegex
, source
)
373 map
[psModuleName
] = path
.resolve(file
)
376 return cache
.psModuleMap
381 function connectIdeServer(psModule
) {
382 const options
= psModule
.options
383 const cache
= psModule
.cache
385 if (cache
.ideServer
) return Promise
.resolve(psModule
)
387 cache
.ideServer
= true
389 const connect
= () => new Promise((resolve
, reject
) => {
390 const args
= dargs(options
.pscIdeArgs
)
392 debug('attempting to connect to psc-ide-server', args
)
394 const ideClient
= spawn('psc-ide-client', args
)
396 ideClient
.stderr
.on('data', data
=> {
397 debug(data
.toString())
398 cache
.ideServer
= false
401 ideClient
.stdout
.once('data', data
=> {
402 debug(data
.toString())
403 if (data
.toString()[0] === '{') {
404 const res
= JSON
.parse(data
.toString())
405 if (res
.resultType
=== 'success') {
406 cache
.ideServer
= ideServer
409 cache
.ideServer
= ideServer
413 cache
.ideServer
= false
417 ideClient
.stdin
.resume()
418 ideClient
.stdin
.write(JSON
.stringify({ command: 'load' }))
419 ideClient
.stdin
.write('\n')
422 const args
= dargs(Object
.assign({
423 outputDirectory: options
.output
,
424 }, options
.pscIdeArgs
))
426 debug('attempting to start psc-ide-server', args
)
428 const ideServer
= cache
.ideServer
= spawn('psc-ide-server', [])
429 ideServer
.stderr
.on('data', data
=> {
430 debug(data
.toString())
433 return retryPromise((retry
, number
) => {
434 return connect().catch(error
=> {
435 if (!cache
.ideServer
&& number
=== 9) {
439 'failed to connect to or start psc-ide-server, ' +
440 'full compilation will occur on rebuild'
443 return Promise
.resolve(psModule
)
456 function match(regex
, str
) {
457 const matches
= str
.match(regex
)
458 return matches
&& matches
[1]
461 function dargs(obj
) {
462 return Object
.keys(obj
).reduce((args
, key
) => {
463 const arg
= '--' + key
.replace(/[A-Z]/g, '-$&').toLowerCase();
466 if (key
=== '_') val
.forEach(v
=> args
.push(v
))
467 else if (Array
.isArray(val
)) val
.forEach(v
=> args
.push(arg
, v
))
468 else args
.push(arg
, obj
[key
])