const globby = require('globby')
const Promise = require('bluebird')
const fs = Promise.promisifyAll(require('fs'))
-const spawn = require('child_process').spawn
+const spawn = require('cross-spawn')
const path = require('path')
const retryPromise = require('promise-retry')
+const jsStringEscape = require('js-string-escape')
-const psModuleRegex = /(?:^|\n)module\s+([\w\.]+)/i
+const ffiModuleRegex = /\/\/\s+module\s+([\w\.]+)/i
+const srcModuleRegex = /(?:^|\n)module\s+([\w\.]+)/i
const requireRegex = /require\(['"]\.\.\/([\w\.]+)['"]\)/g
module.exports = function purescriptLoader(source, map) {
pscArgs: {},
pscBundle: 'psc-bundle',
pscBundleArgs: {},
+ pscIde: false,
pscIdeColors: webpackOptions.psc === 'psa' || query.psc === 'psa',
pscIdeArgs: {},
bundleOutput: 'output/bundle.js',
// invalidate loader cache when bundle is marked as invalid (in watch mode)
this._compiler.plugin('invalid', () => {
cache = config.purescriptLoaderCache = {
- rebuild: true,
+ rebuild: options.pscIde,
deferred: [],
ideServer: cache.ideServer
}
// add psc warnings to webpack compilation warnings
this._compiler.plugin('after-compile', (compilation, callback) => {
- if (options.warnings && cache.warnings && cache.warnings.length) {
- compilation.warnings.unshift(`PureScript compilation:\n${cache.warnings.join('')}`)
+ if (options.warnings && cache.warnings) {
+ compilation.warnings.unshift(`PureScript compilation:\n${cache.warnings}`)
}
- if (cache.errors && cache.errors.length) {
- compilation.errors.unshift(`PureScript compilation:\n${cache.errors.join('\n')}`)
+ if (cache.errors) {
+ compilation.errors.unshift(`PureScript compilation:\n${cache.errors}`)
}
callback()
})
}
- const psModuleName = match(psModuleRegex, source)
+ const psModuleName = match(srcModuleRegex, source)
const psModule = {
name: psModuleName,
load: js => callback(null, js),
cache: cache,
}
+ debug('loader called', psModule.name)
+
if (options.bundle) {
cache.bundleModules.push(psModule.name)
}
.catch(psModule.reject)
}
- if (cache.compilation && cache.compilation.length) {
+ if (cache.compilationFinished) {
return toJavaScript(psModule).then(psModule.load).catch(psModule.reject)
}
// references to compiled output are valid.
cache.deferred.push(psModule)
- if (!cache.compilation) {
+ if (!cache.compilationStarted) {
return compile(psModule)
.then(() => Promise.map(cache.deferred, psModule => {
if (typeof cache.ideServer === 'object') cache.ideServer.kill()
const bundlePath = path.resolve(options.bundleOutput)
const jsPath = cache.bundle ? bundlePath : psModule.jsPath
- debug('loading JavaScript for', psModule.srcPath)
+ debug('loading JavaScript for', psModule.name)
return Promise.props({
js: fs.readFileAsync(jsPath, 'utf8'),
- psModuleMap: psModuleMap(options.src, cache)
+ psModuleMap: psModuleMap(options, cache)
}).then(result => {
let js = ''
if (options.bundle) {
// if bundling, return a reference to the bundle
js = 'module.exports = require("'
- + path.relative(psModule.srcDir, options.bundleOutput)
+ + jsStringEscape(path.relative(psModule.srcDir, options.bundleOutput))
+ '")["' + psModule.name + '"]'
} else {
// replace require paths to output files generated by psc with paths
// to purescript sources, which are then also run through this loader.
- const foreignRequire = 'require("' + path.resolve(
- path.join(psModule.options.output, psModule.name, 'foreign.js')
- ) + '")'
-
js = result.js
.replace(requireRegex, (m, p1) => {
- return 'require("' + result.psModuleMap[p1] + '")'
+ return 'require("' + jsStringEscape(result.psModuleMap[p1].src) + '")'
+ })
+ .replace(/require\(['"]\.\/foreign['"]\)/g, (m, p1) => {
+ return 'require("' + jsStringEscape(result.psModuleMap[psModule.name].ffi) + '")'
})
- .replace(/require\(['"]\.\/foreign['"]\)/g, foreignRequire)
}
return js
const cache = psModule.cache
const stderr = []
- if (cache.compilation) return Promise.resolve(cache.compilation)
-
- cache.compilation = []
- cache.warnings = []
- cache.errors = []
+ if (cache.compilationStarted) return Promise.resolve(psModule)
+ cache.compilationStarted = true
const args = dargs(Object.assign({
_: options.src,
const compilation = spawn(options.psc, args)
+ compilation.stdout.on('data', data => stderr.push(data.toString()))
compilation.stderr.on('data', data => stderr.push(data.toString()))
compilation.on('close', code => {
console.log('Finished compiling PureScript.')
+ cache.compilationFinished = true
if (code !== 0) {
- cache.compilation = cache.errors = stderr
+ cache.errors = stderr.join('')
reject(true)
} else {
- cache.compilation = cache.warnings = stderr
+ cache.warnings = stderr.join('')
resolve(psModule)
}
})
const args = dargs(options.pscIdeArgs)
const ideClient = spawn('psc-ide-client', args)
- ideClient.stdout.once('data', data => {
- const res = JSON.parse(data.toString())
- debug(res)
+ var stdout = ''
+ var stderr = ''
+
+ ideClient.stdout.on('data', data => {
+ stdout = stdout + data.toString()
+ })
+
+ ideClient.stderr.on('data', data => {
+ stderr = stderr + data.toString()
+ })
- if (!Array.isArray(res.result)) {
+ ideClient.on('close', code => {
+ if (code !== 0) {
+ const error = stderr === '' ? 'Failed to spawn psc-ide-client' : stderr
+ return reject(new Error(error))
+ }
+
+ let res = null
+
+ try {
+ res = JSON.parse(stdout.toString())
+ debug(res)
+ } catch (err) {
+ return reject(err)
+ }
+
+ if (res && !Array.isArray(res.result)) {
return res.resultType === 'success'
? resolve(psModule)
- : reject(res)
+ : reject('psc-ide rebuild failed')
}
Promise.map(res.result, (item, i) => {
})
.then(compileMessages => {
if (res.resultType === 'error') {
- cache.errors = compileMessages
- reject(res)
+ if (res.result.some(item => item.errorCode === 'UnknownModule')) {
+ console.log('Unknown module, attempting full recompile')
+ return compile(psModule)
+ .then(() => request({ command: 'load' }))
+ .then(resolve)
+ .catch(() => reject('psc-ide rebuild failed'))
+ }
+ cache.errors = compileMessages.join('\n')
+ reject('psc-ide rebuild failed')
} else {
- cache.warnings = compileMessages
+ cache.warnings = compileMessages.join('\n')
resolve(psModule)
}
})
})
- ideClient.stderr.once('data', data => reject(data.toString()))
-
ideClient.stdin.write(JSON.stringify(body))
ideClient.stdin.write('\n')
})
params: {
file: psModule.srcPath,
}
- }).catch(res => {
- if (res.resultType === 'error') {
- if (res.result.some(item => item.errorCode === 'UnknownModule')) {
- console.log('Unknown module, attempting full recompile')
- return compile(psModule).then(() => request({ command: 'load' }))
- }
- }
- return Promise.resolve(psModule)
})
}
compilation.stderr.on('data', data => stderr.push(data.toString()))
compilation.on('close', code => {
if (code !== 0) {
- cache.errors.concat(stderr)
+ cache.errors = (cache.errors || '') + stderr.join('')
return reject(true)
}
cache.bundle = stderr
}
// map of PS module names to their source path
-function psModuleMap(globs, cache) {
+function psModuleMap(options, cache) {
if (cache.psModuleMap) return Promise.resolve(cache.psModuleMap)
+ const globs = [].concat(options.src).concat(options.ffi)
+
return globby(globs).then(paths => {
return Promise
.props(paths.reduce((map, file) => {
map[file] = fs.readFileAsync(file, 'utf8')
return map
}, {}))
- .then(srcMap => {
- cache.psModuleMap = Object.keys(srcMap).reduce((map, file) => {
- const source = srcMap[file]
- const psModuleName = match(psModuleRegex, source)
- map[psModuleName] = path.resolve(file)
+ .then(fileMap => {
+ cache.psModuleMap = Object.keys(fileMap).reduce((map, file) => {
+ const source = fileMap[file]
+ const ext = path.extname(file)
+ const isPurs = ext.match(/purs$/i)
+ const moduleRegex = isPurs ? srcModuleRegex : ffiModuleRegex
+ const moduleName = match(moduleRegex, source)
+ map[moduleName] = map[moduleName] || {}
+ if (isPurs) {
+ map[moduleName].src = path.resolve(file)
+ } else {
+ map[moduleName].ffi = path.resolve(file)
+ }
return map
}, {})
return cache.psModuleMap
else if (Array.isArray(val)) val.forEach(v => args.push(arg, v))
else args.push(arg, obj[key])
- return args
+ return args.filter(arg => (typeof arg !== 'boolean'))
}, [])
}