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
&& cache
.warnings
.length
) {
69 compilation
.warnings
.unshift(`PureScript compilation:\n${cache.warnings.join('')}`)
72 if (cache
.errors
&& cache
.errors
.length
) {
73 compilation
.errors
.unshift(`PureScript compilation:\n${cache.errors.join('\n')}`)
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
.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
.compilation
= cache
.errors
= stderr
196 cache
.compilation
= cache
.warnings
= stderr
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
)
219 ideClient
.stdout
.once('data', data
=> {
223 res
= JSON
.parse(data
.toString())
229 if (res
&& !Array
.isArray(res
.result
)) {
230 return res
.resultType
=== 'success'
232 : reject('psc-ide rebuild failed')
235 Promise
.map(res
.result
, (item
, i
) => {
237 return formatIdeResult(item
, options
, i
, res
.result
.length
)
239 .then(compileMessages
=> {
240 if (res
.resultType
=== 'error') {
241 if (res
.result
.some(item
=> item
.errorCode
=== 'UnknownModule')) {
242 console
.log('Unknown module, attempting full recompile')
243 return compile(psModule
)
244 .then(() => request({ command: 'load' }))
246 .catch(() => reject('psc-ide rebuild failed'))
248 cache
.errors
= compileMessages
249 reject('psc-ide rebuild failed')
251 cache
.warnings
= compileMessages
257 ideClient
.stderr
.once('data', data
=> reject(data
.toString()))
259 ideClient
.stdin
.write(JSON
.stringify(body
))
260 ideClient
.stdin
.write('\n')
266 file: psModule
.srcPath
,
271 function formatIdeResult(result
, options
, index
, length
) {
272 const srcPath
= path
.relative(options
.context
, result
.filename
)
273 const pos
= result
.position
274 const fileAndPos
= `${srcPath}:${pos.startLine}:${pos.startColumn}`
275 let numAndErr
= `[${index+1}/${length} ${result.errorCode}]`
276 numAndErr
= options
.pscIdeColors
? colors
.yellow(numAndErr
) : numAndErr
278 return fs
.readFileAsync(result
.filename
, 'utf8').then(source
=> {
279 const lines
= source
.split('\n').slice(pos
.startLine
- 1, pos
.endLine
)
280 const endsOnNewline
= pos
.endColumn
=== 1 && pos
.startLine
!== pos
.endLine
281 const up
= options
.pscIdeColors
? colors
.red('^') : '^'
282 const down
= options
.pscIdeColors
? colors
.red('v') : 'v'
283 let trimmed
= lines
.slice(0)
286 lines
.splice(lines
.length
- 1, 1)
287 pos
.endLine
= pos
.endLine
- 1
288 pos
.endColumn
= lines
[lines
.length
- 1].length
|| 1
291 // strip newlines at the end
293 trimmed
= lines
.reverse().reduce((trimmed
, line
, i
) => {
294 if (i
=== 0 && line
=== '') trimmed
.trimming
= true
295 if (!trimmed
.trimming
) trimmed
.push(line
)
296 if (trimmed
.trimming
&& line
!== '') {
297 trimmed
.trimming
= false
302 pos
.endLine
= pos
.endLine
- (lines
.length
- trimmed
.length
)
303 pos
.endColumn
= trimmed
[trimmed
.length
- 1].length
|| 1
306 const spaces
= ' '.repeat(String(pos
.endLine
).length
)
307 let snippet
= trimmed
.map((line
, i
) => {
308 return ` ${pos.startLine + i} ${line}`
311 if (trimmed
.length
=== 1) {
312 snippet
+= `\n ${spaces} ${' '.repeat(pos.startColumn - 1)}${up.repeat(pos.endColumn - pos.startColumn + 1)}`
314 snippet
= ` ${spaces} ${' '.repeat(pos.startColumn - 1)}${down}\n${snippet}`
315 snippet
+= `\n ${spaces} ${' '.repeat(pos.endColumn - 1)}${up}`
318 return Promise
.resolve(
319 `\n${numAndErr} ${fileAndPos}\n\n${snippet}\n\n${result.message}`
324 function bundle(options
, cache
) {
325 if (cache
.bundle
) return Promise
.resolve(cache
.bundle
)
328 const stderr
= cache
.bundle
= []
330 const args
= dargs(Object
.assign({
331 _: [path
.join(options
.output
, '*', '*.js')],
332 output: options
.bundleOutput
,
333 namespace: options
.bundleNamespace
,
334 }, options
.pscBundleArgs
))
336 cache
.bundleModules
.forEach(name
=> args
.push('--module', name
))
338 debug('spawning bundler %s %o', options
.pscBundle
, args
.join(' '))
340 return (new Promise((resolve
, reject
) => {
341 console
.log('Bundling PureScript...')
343 const compilation
= spawn(options
.pscBundle
, args
)
345 compilation
.stdout
.on('data', data
=> stdout
.push(data
.toString()))
346 compilation
.stderr
.on('data', data
=> stderr
.push(data
.toString()))
347 compilation
.on('close', code
=> {
349 cache
.errors
.concat(stderr
)
352 cache
.bundle
= stderr
353 resolve(fs
.appendFileAsync('output/bundle.js', `module.exports = ${options.bundleNamespace}`))
358 // map of PS module names to their source path
359 function psModuleMap(globs
, cache
) {
360 if (cache
.psModuleMap
) return Promise
.resolve(cache
.psModuleMap
)
362 return globby(globs
).then(paths
=> {
364 .props(paths
.reduce((map
, file
) => {
365 map
[file
] = fs
.readFileAsync(file
, 'utf8')
369 cache
.psModuleMap
= Object
.keys(srcMap
).reduce((map
, file
) => {
370 const source
= srcMap
[file
]
371 const psModuleName
= match(psModuleRegex
, source
)
372 map
[psModuleName
] = path
.resolve(file
)
375 return cache
.psModuleMap
380 function connectIdeServer(psModule
) {
381 const options
= psModule
.options
382 const cache
= psModule
.cache
384 if (cache
.ideServer
) return Promise
.resolve(psModule
)
386 cache
.ideServer
= true
388 const connect
= () => new Promise((resolve
, reject
) => {
389 const args
= dargs(options
.pscIdeArgs
)
391 debug('attempting to connect to psc-ide-server', args
)
393 const ideClient
= spawn('psc-ide-client', args
)
395 ideClient
.stderr
.on('data', data
=> {
396 debug(data
.toString())
397 cache
.ideServer
= false
400 ideClient
.stdout
.once('data', data
=> {
401 debug(data
.toString())
402 if (data
.toString()[0] === '{') {
403 const res
= JSON
.parse(data
.toString())
404 if (res
.resultType
=== 'success') {
405 cache
.ideServer
= ideServer
408 cache
.ideServer
= ideServer
412 cache
.ideServer
= false
416 ideClient
.stdin
.resume()
417 ideClient
.stdin
.write(JSON
.stringify({ command: 'load' }))
418 ideClient
.stdin
.write('\n')
421 const args
= dargs(Object
.assign({
422 outputDirectory: options
.output
,
423 }, options
.pscIdeArgs
))
425 debug('attempting to start psc-ide-server', args
)
427 const ideServer
= cache
.ideServer
= spawn('psc-ide-server', [])
428 ideServer
.stderr
.on('data', data
=> {
429 debug(data
.toString())
432 return retryPromise((retry
, number
) => {
433 return connect().catch(error
=> {
434 if (!cache
.ideServer
&& number
=== 9) {
438 'failed to connect to or start psc-ide-server, ' +
439 'full compilation will occur on rebuild'
442 return Promise
.resolve(psModule
)
455 function match(regex
, str
) {
456 const matches
= str
.match(regex
)
457 return matches
&& matches
[1]
460 function dargs(obj
) {
461 return Object
.keys(obj
).reduce((args
, key
) => {
462 const arg
= '--' + key
.replace(/[A-Z]/g, '-$&').toLowerCase();
465 if (key
=== '_') val
.forEach(v
=> args
.push(v
))
466 else if (Array
.isArray(val
)) val
.forEach(v
=> args
.push(arg
, v
))
467 else args
.push(arg
, obj
[key
])