From 7de41f10b4ff0f0d6b45d59bee0f166c3cfe3f9f Mon Sep 17 00:00:00 2001 From: Alex Mingoia Date: Tue, 10 May 2016 00:09:28 -0700 Subject: Refactor to compile independently of purescript-webpack-plugin. - Remove dependence on purescript-webpack-plugin - Fixes double-compilation issue by loading compiled JS instead of adding dependency. - Uses `psc-ide-server` for fast rebuilds. --- src/PursLoader/Debug.js | 12 - src/PursLoader/Debug.purs | 9 - src/PursLoader/JsStringEscape.js | 7 - src/PursLoader/JsStringEscape.purs | 3 - src/PursLoader/Loader.purs | 108 --------- src/PursLoader/LoaderRef.js | 50 ---- src/PursLoader/LoaderRef.purs | 40 ---- src/PursLoader/Path.js | 24 -- src/PursLoader/Path.purs | 14 -- src/PursLoader/Plugin.js | 14 -- src/PursLoader/Plugin.purs | 34 --- src/index.js | 465 +++++++++++++++++++++++++++++++++++++ 12 files changed, 465 insertions(+), 315 deletions(-) delete mode 100644 src/PursLoader/Debug.js delete mode 100644 src/PursLoader/Debug.purs delete mode 100644 src/PursLoader/JsStringEscape.js delete mode 100644 src/PursLoader/JsStringEscape.purs delete mode 100644 src/PursLoader/Loader.purs delete mode 100644 src/PursLoader/LoaderRef.js delete mode 100644 src/PursLoader/LoaderRef.purs delete mode 100644 src/PursLoader/Path.js delete mode 100644 src/PursLoader/Path.purs delete mode 100644 src/PursLoader/Plugin.js delete mode 100644 src/PursLoader/Plugin.purs create mode 100644 src/index.js (limited to 'src') diff --git a/src/PursLoader/Debug.js b/src/PursLoader/Debug.js deleted file mode 100644 index 85eca10..0000000 --- a/src/PursLoader/Debug.js +++ /dev/null @@ -1,12 +0,0 @@ -'use strict'; - -// module PursLoader.Debug - -var debug_ = require('debug')('purs-loader'); - -function debug(message) { - return function(){ - debug_(message); - }; -} -exports.debug = debug; diff --git a/src/PursLoader/Debug.purs b/src/PursLoader/Debug.purs deleted file mode 100644 index 7a02f69..0000000 --- a/src/PursLoader/Debug.purs +++ /dev/null @@ -1,9 +0,0 @@ -module PursLoader.Debug (debug) where - -import Prelude (Unit()) - -import Control.Monad.Eff (Eff()) - -import PursLoader.LoaderRef (Loader()) - -foreign import debug :: forall eff. String -> Eff (loader :: Loader | eff) Unit diff --git a/src/PursLoader/JsStringEscape.js b/src/PursLoader/JsStringEscape.js deleted file mode 100644 index ff0a1a6..0000000 --- a/src/PursLoader/JsStringEscape.js +++ /dev/null @@ -1,7 +0,0 @@ -'use strict'; - -// module PursLoader.JsStringEscape - -var jsStringEscape = require('js-string-escape'); - -exports.jsStringEscape = jsStringEscape; diff --git a/src/PursLoader/JsStringEscape.purs b/src/PursLoader/JsStringEscape.purs deleted file mode 100644 index 79590ae..0000000 --- a/src/PursLoader/JsStringEscape.purs +++ /dev/null @@ -1,3 +0,0 @@ -module PursLoader.JsStringEscape (jsStringEscape) where - -foreign import jsStringEscape :: String -> String diff --git a/src/PursLoader/Loader.purs b/src/PursLoader/Loader.purs deleted file mode 100644 index acb0993..0000000 --- a/src/PursLoader/Loader.purs +++ /dev/null @@ -1,108 +0,0 @@ -module PursLoader.Loader - ( Effects() - , Effects_() - , loader - , loaderFn - ) where - -import Prelude (Unit(), ($), (>>=), (<$>), (<*>), (++), (<<<), bind, const, id, pure, unit) - -import Control.Bind (join) -import Control.Monad.Eff (Eff(), foreachE) -import Control.Monad.Eff.Console (CONSOLE()) -import Control.Monad.Eff.Exception (EXCEPTION(), Error(), error) - -import Data.Array ((!!)) -import Data.Either (Either(..), either) -import Data.Function (Fn2(), mkFn2) -import Data.Maybe (maybe) -import Data.Nullable (toMaybe) -import Data.String.Regex (Regex(), match, noFlags, regex) - -import Unsafe.Coerce (unsafeCoerce) - -import PursLoader.Debug (debug) -import PursLoader.JsStringEscape (jsStringEscape) -import PursLoader.LoaderRef - ( AsyncCallback() - , LoaderRef() - , Loader() - , async - , cacheable - , addDependency - , resourcePath - ) -import PursLoader.Path (dirname, joinPath, relative) -import PursLoader.Plugin as Plugin - -type Effects eff = (console :: CONSOLE, err :: EXCEPTION | eff) - -type Effects_ eff = Effects (loader :: Loader | eff) - -loader :: forall eff. LoaderRef -> String -> Eff (Effects_ eff) Unit -loader ref source = do - callback <- async ref - - cacheable ref - - debug "Invoke PureScript plugin compilation" - - pluginContext.compile (compile callback) - where - pluginContext :: Plugin.Context (Effects_ eff) - pluginContext = (unsafeCoerce ref).purescriptWebpackPluginContext - - compile :: AsyncCallback (Effects eff) -> Plugin.Compile (Effects_ eff) - compile callback error' graph = do - either (const $ pure unit) (\a -> debug ("Adding PureScript dependency " ++ a)) name - - addDependency ref (resourcePath ref) - - either (const $ callback (pure fixedError) "") id - (handle <$> name <*> dependencies <*> exports) - where - fixedError :: Error - fixedError = error "PureScript compilation has failed." - - handle :: String -> Array String -> String -> Eff (Effects_ eff) Unit - handle name' deps res = do - debug ("Adding PureScript dependencies for " ++ name') - foreachE deps (addDependency ref) - debug "Generated loader result" - debug res - callback (const fixedError <$> toMaybe error') res - - exports :: Either Error String - exports = - if pluginContext.options.bundle - then bundleExport <$> name - else moduleExport <<< modulePath <$> name - where - bundleExport :: String -> String - bundleExport name' = "module.exports = require('" ++ jsStringEscape path ++ "')['" ++ name' ++ "'];" - where - path :: String - path = relative resourceDir pluginContext.options.bundleOutput - - moduleExport :: String -> String - moduleExport path = "module.exports = require('" ++ jsStringEscape path ++ "');" - - modulePath :: String -> String - modulePath = relative resourceDir <<< joinPath pluginContext.options.output - - resourceDir :: String - resourceDir = dirname (resourcePath ref) - - dependencies :: Either Error (Array String) - dependencies = Plugin.dependenciesOf graph (resourcePath ref) - - name :: Either Error String - name = - maybe (Left $ error "Failed to parse module name") Right - (join $ match re source >>= \as -> as !! 1) - where - re :: Regex - re = regex "(?:^|\\n)module\\s+([\\w\\.]+)" noFlags { ignoreCase = true } - -loaderFn :: forall eff. Fn2 LoaderRef String (Eff (Effects_ eff) Unit) -loaderFn = mkFn2 loader diff --git a/src/PursLoader/LoaderRef.js b/src/PursLoader/LoaderRef.js deleted file mode 100644 index a5d8e1f..0000000 --- a/src/PursLoader/LoaderRef.js +++ /dev/null @@ -1,50 +0,0 @@ -'use strict'; - -// module PursLoader.LoaderRef - -function asyncFn(isJust, fromMaybe, ref){ - return function(){ - var callback = ref.async(); - return function(error){ - return function(value){ - return function(){ - return isJust(error) ? callback(fromMaybe(new Error())(error)) - : callback(null, value); - }; - }; - }; - }; -} -function cacheable(ref){ - return function(){ - return ref.cacheable && ref.cacheable(); - }; -} - -function clearDependencies(ref){ - return function(){ - return ref.clearDependencies(); - }; -} - -function resourcePath(ref){ - return ref.resourcePath; -} - -function addDependency(ref){ - return function(dep){ - return function(){ - return ref.addDependency(dep); - }; - }; -} - -exports.asyncFn = asyncFn; - -exports.cacheable = cacheable; - -exports.clearDependencies = clearDependencies; - -exports.resourcePath = resourcePath; - -exports.addDependency = addDependency; diff --git a/src/PursLoader/LoaderRef.purs b/src/PursLoader/LoaderRef.purs deleted file mode 100644 index 140d94a..0000000 --- a/src/PursLoader/LoaderRef.purs +++ /dev/null @@ -1,40 +0,0 @@ -module PursLoader.LoaderRef - ( LoaderRef() - , Loader() - , AsyncCallback() - , async - , cacheable - , clearDependencies - , addDependency - , resourcePath - ) where - -import Prelude (Unit()) - -import Control.Monad.Eff (Eff()) -import Control.Monad.Eff.Exception (Error()) - -import Data.Function (Fn3(), runFn3) -import Data.Maybe (Maybe(), fromMaybe, isJust) - -type AsyncCallback eff = Maybe Error -> String -> Eff (loader :: Loader | eff) Unit - -data LoaderRef - -foreign import data Loader :: ! - -foreign import asyncFn :: forall eff. Fn3 (Maybe Error -> Boolean) - (Error -> Maybe Error -> Error) - LoaderRef - (Eff (loader :: Loader | eff) (AsyncCallback eff)) - -async :: forall eff. LoaderRef -> Eff (loader :: Loader | eff) (Maybe Error -> String -> Eff (loader :: Loader | eff) Unit) -async ref = runFn3 asyncFn isJust fromMaybe ref - -foreign import cacheable :: forall eff. LoaderRef -> Eff (loader :: Loader | eff) Unit - -foreign import clearDependencies :: forall eff. LoaderRef -> Eff (loader :: Loader | eff) Unit - -foreign import resourcePath :: LoaderRef -> String - -foreign import addDependency :: forall eff. LoaderRef -> String -> Eff (loader :: Loader | eff) Unit diff --git a/src/PursLoader/Path.js b/src/PursLoader/Path.js deleted file mode 100644 index 878f256..0000000 --- a/src/PursLoader/Path.js +++ /dev/null @@ -1,24 +0,0 @@ -'use strict' - -// module PursLoader.Path - -var path = require('path'); - -function relative(from) { - return function(to){ - return path.relative(from, to); - }; -} -exports.relative = relative; - - -function joinPath(a) { - return function(b) { - return path.join(a, b); - }; -} -exports.joinPath = joinPath; - -exports.resolve = path.resolve; - -exports.dirname = path.dirname; diff --git a/src/PursLoader/Path.purs b/src/PursLoader/Path.purs deleted file mode 100644 index 98cad5a..0000000 --- a/src/PursLoader/Path.purs +++ /dev/null @@ -1,14 +0,0 @@ -module PursLoader.Path - ( relative - , resolve - , dirname - , joinPath - ) where - -foreign import relative :: String -> String -> String - -foreign import resolve :: String -> String - -foreign import dirname :: String -> String - -foreign import joinPath :: String -> String -> String diff --git a/src/PursLoader/Plugin.js b/src/PursLoader/Plugin.js deleted file mode 100644 index ded6df5..0000000 --- a/src/PursLoader/Plugin.js +++ /dev/null @@ -1,14 +0,0 @@ -'use strict'; - -// module PursLoader.Plugin - -function dependenciesOfFn(left, right, graph, node) { - try { - var dependencies = graph.dependenciesOf(node); - return right(dependencies); - } - catch (error) { - return left(error); - } -} -exports.dependenciesOfFn = dependenciesOfFn; diff --git a/src/PursLoader/Plugin.purs b/src/PursLoader/Plugin.purs deleted file mode 100644 index c798c83..0000000 --- a/src/PursLoader/Plugin.purs +++ /dev/null @@ -1,34 +0,0 @@ -module PursLoader.Plugin - ( Compile() - , Context() - , Options() - , DependencyGraph() - , dependenciesOf - ) where - -import Prelude (Unit()) - -import Control.Monad.Eff (Eff()) -import Control.Monad.Eff.Exception (Error()) - -import Data.Either (Either(..)) -import Data.Function (Fn4(), runFn4) -import Data.Nullable (Nullable()) - -type Compile eff = Nullable Error -> DependencyGraph -> Eff eff Unit - -type Context eff = { compile :: Compile eff -> Eff eff Unit, options :: Options } - -type Options = { bundle :: Boolean, output :: String, bundleOutput :: String } - -dependenciesOf :: DependencyGraph -> String -> Either Error (Array String) -dependenciesOf = runFn4 dependenciesOfFn Left Right - -foreign import data DependencyGraph :: * - -foreign import dependenciesOfFn - :: Fn4 (Error -> Either Error (Array String)) - (Array String -> Either Error (Array String)) - DependencyGraph - String - (Either Error (Array String)) diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..0a25ccd --- /dev/null +++ b/src/index.js @@ -0,0 +1,465 @@ +'use strict' + +const colors = require('chalk') +const debug = require('debug')('purs-loader') +const loaderUtils = require('loader-utils') +const globby = require('globby') +const Promise = require('bluebird') +const fs = Promise.promisifyAll(require('fs')) +const spawn = require('child_process').spawn +const path = require('path') +const retryPromise = require('promise-retry') + +const psModuleRegex = /(?:^|\n)module\s+([\w\.]+)/i +const requireRegex = /require\(['"]\.\.\/([\w\.]+)['"]\)/g + +module.exports = function purescriptLoader(source, map) { + const callback = this.async() + const config = this.options + const query = loaderUtils.parseQuery(this.query) + const webpackOptions = this.options.purescriptLoader || {} + + const options = Object.assign({ + context: config.context, + psc: 'psc', + pscArgs: {}, + pscBundle: 'psc-bundle', + pscBundleArgs: {}, + pscIdeColors: webpackOptions.psc === 'psa' || query.psc === 'psa', + pscIdeArgs: {}, + bundleOutput: 'output/bundle.js', + bundleNamespace: 'PS', + bundle: false, + warnings: true, + output: 'output', + src: [ + path.join('src', '**', '*.purs'), + path.join('bower_components', 'purescript-*', 'src', '**', '*.purs') + ], + ffi: [ + path.join('src', '**', '*.js'), + path.join('bower_components', 'purescript-*', 'src', '**', '*.js') + ], + }, webpackOptions, query) + + this.cacheable && this.cacheable() + + let cache = config.purescriptLoaderCache = config.purescriptLoaderCache || { + rebuild: false, + deferred: [], + bundleModules: [], + } + + if (!config.purescriptLoaderInstalled) { + config.purescriptLoaderInstalled = true + + // invalidate loader cache when bundle is marked as invalid (in watch mode) + this._compiler.plugin('invalid', () => { + cache = config.purescriptLoaderCache = { + rebuild: true, + 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 (cache.errors && cache.errors.length) { + compilation.errors.unshift(`PureScript compilation:\n${cache.errors.join('\n')}`) + } + + callback() + }) + } + + const psModuleName = match(psModuleRegex, source) + const psModule = { + name: psModuleName, + load: js => callback(null, js), + reject: error => callback(error), + srcPath: this.resourcePath, + srcDir: path.dirname(this.resourcePath), + jsPath: path.resolve(path.join(options.output, psModuleName, 'index.js')), + options: options, + cache: cache, + } + + if (options.bundle) { + cache.bundleModules.push(psModule.name) + } + + if (cache.rebuild) { + return connectIdeServer(psModule) + .then(rebuild) + .then(toJavaScript) + .then(psModule.load) + .catch(psModule.reject) + } + + if (cache.compilation && cache.compilation.length) { + return toJavaScript(psModule).then(psModule.load).catch(psModule.reject) + } + + // We need to wait for compilation to finish before the loaders run so that + // references to compiled output are valid. + cache.deferred.push(psModule) + + if (!cache.compilation) { + return compile(psModule) + .then(() => Promise.map(cache.deferred, psModule => { + if (typeof cache.ideServer === 'object') cache.ideServer.kill() + return toJavaScript(psModule).then(psModule.load) + })) + .catch(error => { + cache.deferred[0].reject(error) + cache.deferred.slice(1).forEach(psModule => psModule.reject(true)) + }) + } +} + +// The actual loader is executed *after* purescript compilation. +function toJavaScript(psModule) { + const options = psModule.options + const cache = psModule.cache + const bundlePath = path.resolve(options.bundleOutput) + const jsPath = cache.bundle ? bundlePath : psModule.jsPath + + debug('loading JavaScript for', psModule.srcPath) + + return Promise.props({ + js: fs.readFileAsync(jsPath, 'utf8'), + psModuleMap: psModuleMap(options.src, 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) + + '")["' + 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] + '")' + }) + .replace(/require\(['"]\.\/foreign['"]\)/g, foreignRequire) + } + + return js + }) +} + +function compile(psModule) { + const options = psModule.options + const cache = psModule.cache + const stderr = [] + + if (cache.compilation) return Promise.resolve(cache.compilation) + + cache.compilation = [] + cache.warnings = [] + cache.errors = [] + + + const args = dargs(Object.assign({ + _: options.src, + ffi: options.ffi, + output: options.output, + }, options.pscArgs)) + + debug('spawning compiler %s %o', options.psc, args) + + return (new Promise((resolve, reject) => { + console.log('\nCompiling PureScript...') + + const compilation = spawn(options.psc, args) + + compilation.stderr.on('data', data => stderr.push(data.toString())) + + compilation.on('close', code => { + console.log('Finished compiling PureScript.') + if (code !== 0) { + cache.compilation = cache.errors = stderr + reject(true) + } else { + cache.compilation = cache.warnings = stderr + resolve(psModule) + } + }) + })) + .then(compilerOutput => { + if (options.bundle) { + return bundle(options, cache).then(() => psModule) + } + return psModule + }) +} + +function rebuild(psModule) { + const options = psModule.options + const cache = psModule.cache + + debug('attempting rebuild with psc-ide-client %s', psModule.srcPath) + + const request = (body) => new Promise((resolve, reject) => { + 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) + + if (!Array.isArray(res.result)) { + return res.resultType === 'success' + ? resolve(psModule) + : reject(res.result) + } + + Promise.map(res.result, (item, i) => { + debug(item) + return formatIdeResult(item, options, i, res.result.length) + }) + .then(compileMessages => { + if (res.resultType === 'error') { + cache.errors = compileMessages + reject(res.result) + } else { + cache.warnings = compileMessages + resolve(psModule) + } + }) + }) + + ideClient.stderr.once('data', data => reject(data.toString())) + + ideClient.stdin.write(JSON.stringify(body)) + ideClient.stdin.write('\n') + }) + + return request({ + command: 'rebuild', + 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) + }) +} + +function formatIdeResult(result, options, index, length) { + const srcPath = path.relative(options.context, result.filename) + const pos = result.position + const fileAndPos = `${srcPath}:${pos.startLine}:${pos.startColumn}` + let numAndErr = `[${index+1}/${length} ${result.errorCode}]` + numAndErr = options.pscIdeColors ? colors.yellow(numAndErr) : numAndErr + + return fs.readFileAsync(result.filename, 'utf8').then(source => { + const lines = source.split('\n').slice(pos.startLine - 1, pos.endLine) + const endsOnNewline = pos.endColumn === 1 && pos.startLine !== pos.endLine + const up = options.pscIdeColors ? colors.red('^') : '^' + const down = options.pscIdeColors ? colors.red('v') : 'v' + let trimmed = lines.slice(0) + + if (endsOnNewline) { + lines.splice(lines.length - 1, 1) + pos.endLine = pos.endLine - 1 + pos.endColumn = lines[lines.length - 1].length || 1 + } + + // strip newlines at the end + if (endsOnNewline) { + trimmed = lines.reverse().reduce((trimmed, line, i) => { + if (i === 0 && line === '') trimmed.trimming = true + if (!trimmed.trimming) trimmed.push(line) + if (trimmed.trimming && line !== '') { + trimmed.trimming = false + trimmed.push(line) + } + return trimmed + }, []).reverse() + pos.endLine = pos.endLine - (lines.length - trimmed.length) + pos.endColumn = trimmed[trimmed.length - 1].length || 1 + } + + const spaces = ' '.repeat(String(pos.endLine).length) + let snippet = trimmed.map((line, i) => { + return ` ${pos.startLine + i} ${line}` + }).join('\n') + + if (trimmed.length === 1) { + snippet += `\n ${spaces} ${' '.repeat(pos.startColumn - 1)}${up.repeat(pos.endColumn - pos.startColumn + 1)}` + } else { + snippet = ` ${spaces} ${' '.repeat(pos.startColumn - 1)}${down}\n${snippet}` + snippet += `\n ${spaces} ${' '.repeat(pos.endColumn - 1)}${up}` + } + + return Promise.resolve( + `\n${numAndErr} ${fileAndPos}\n\n${snippet}\n\n${result.message}` + ) + }) +} + +function bundle(options, cache) { + if (cache.bundle) return Promise.resolve(cache.bundle) + + const stdout = [] + const stderr = cache.bundle = [] + + const args = dargs(Object.assign({ + _: [path.join(options.output, '*', '*.js')], + output: options.bundleOutput, + namespace: options.bundleNamespace, + }, options.pscBundleArgs)) + + cache.bundleModules.forEach(name => args.push('--module', name)) + + debug('spawning bundler %s %o', options.pscBundle, args.join(' ')) + + return (new Promise((resolve, reject) => { + console.log('Bundling PureScript...') + + const compilation = spawn(options.pscBundle, args) + + compilation.stdout.on('data', data => stdout.push(data.toString())) + compilation.stderr.on('data', data => stderr.push(data.toString())) + compilation.on('close', code => { + if (code !== 0) { + cache.errors.concat(stderr) + return reject(true) + } + cache.bundle = stderr + resolve(fs.appendFileAsync('output/bundle.js', `module.exports = ${options.bundleNamespace}`)) + }) + })) +} + +// map of PS module names to their source path +function psModuleMap(globs, cache) { + if (cache.psModuleMap) return Promise.resolve(cache.psModuleMap) + + 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) + return map + }, {}) + return cache.psModuleMap + }) + }) +} + +function connectIdeServer(psModule) { + const options = psModule.options + const cache = psModule.cache + + if (cache.ideServer) return Promise.resolve(psModule) + + cache.ideServer = true + + const connect = () => new Promise((resolve, reject) => { + const args = dargs(options.pscIdeArgs) + + debug('attempting to connect to psc-ide-server', args) + + const ideClient = spawn('psc-ide-client', args) + + ideClient.stderr.on('data', data => { + debug(data.toString()) + cache.ideServer = false + reject(true) + }) + ideClient.stdout.once('data', data => { + debug(data.toString()) + if (data.toString()[0] === '{') { + const res = JSON.parse(data.toString()) + if (res.resultType === 'success') { + cache.ideServer = ideServer + resolve(psModule) + } else { + cache.ideServer = ideServer + reject(true) + } + } else { + cache.ideServer = false + reject(true) + } + }) + ideClient.stdin.resume() + ideClient.stdin.write(JSON.stringify({ command: 'load' })) + ideClient.stdin.write('\n') + }) + + const args = dargs(Object.assign({ + outputDirectory: options.output, + }, options.pscIdeArgs)) + + debug('attempting to start psc-ide-server', args) + + const ideServer = cache.ideServer = spawn('psc-ide-server', []) + ideServer.stderr.on('data', data => { + debug(data.toString()) + }) + + return retryPromise((retry, number) => { + return connect().catch(error => { + if (!cache.ideServer && number === 9) { + debug(error) + + console.log( + 'failed to connect to or start psc-ide-server, ' + + 'full compilation will occur on rebuild' + ) + + return Promise.resolve(psModule) + } + + return retry(error) + }) + }, { + retries: 9, + factor: 1, + minTimeout: 333, + maxTimeout: 333, + }) +} + +function match(regex, str) { + const matches = str.match(regex) + return matches && matches[1] +} + +function dargs(obj) { + return Object.keys(obj).reduce((args, key) => { + const arg = '--' + key.replace(/[A-Z]/g, '-$&').toLowerCase(); + const val = obj[key] + + if (key === '_') val.forEach(v => args.push(v)) + else if (Array.isArray(val)) val.forEach(v => args.push(arg, v)) + else args.push(arg, obj[key]) + + return args + }, []) +} -- cgit v1.2.3