aboutsummaryrefslogtreecommitdiffhomepage
path: root/src
diff options
context:
space:
mode:
authoreric <thul.eric@gmail.com>2016-05-21 22:44:27 -0400
committereric <thul.eric@gmail.com>2016-05-21 22:44:27 -0400
commitd301b47d8fb7139d11f59c5d969bdde6132a8230 (patch)
tree10a7677d88c750b360f141b2e9d5ca095e01ab4c /src
parent777472b3830cb3d2ff3390003ea422c6d4522715 (diff)
parent17acb575860cf1bed9e1f6d992a9b7cd66057464 (diff)
downloadpurs-loader-d301b47d8fb7139d11f59c5d969bdde6132a8230.tar.gz
purs-loader-d301b47d8fb7139d11f59c5d969bdde6132a8230.tar.zst
purs-loader-d301b47d8fb7139d11f59c5d969bdde6132a8230.zip
Merge pull request #47 from alexmingoia/topic/refactor
Refactor to compile independently of purescript-webpack-plugin.
Diffstat (limited to 'src')
-rw-r--r--src/PursLoader/Debug.js12
-rw-r--r--src/PursLoader/Debug.purs9
-rw-r--r--src/PursLoader/JsStringEscape.js7
-rw-r--r--src/PursLoader/JsStringEscape.purs3
-rw-r--r--src/PursLoader/Loader.purs108
-rw-r--r--src/PursLoader/LoaderRef.js50
-rw-r--r--src/PursLoader/LoaderRef.purs40
-rw-r--r--src/PursLoader/Path.js24
-rw-r--r--src/PursLoader/Path.purs14
-rw-r--r--src/PursLoader/Plugin.js14
-rw-r--r--src/PursLoader/Plugin.purs34
-rw-r--r--src/index.js481
12 files changed, 481 insertions, 315 deletions
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 @@
1'use strict';
2
3// module PursLoader.Debug
4
5var debug_ = require('debug')('purs-loader');
6
7function debug(message) {
8 return function(){
9 debug_(message);
10 };
11}
12exports.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 @@
1module PursLoader.Debug (debug) where
2
3import Prelude (Unit())
4
5import Control.Monad.Eff (Eff())
6
7import PursLoader.LoaderRef (Loader())
8
9foreign 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 @@
1'use strict';
2
3// module PursLoader.JsStringEscape
4
5var jsStringEscape = require('js-string-escape');
6
7exports.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 @@
1module PursLoader.JsStringEscape (jsStringEscape) where
2
3foreign 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 @@
1module PursLoader.Loader
2 ( Effects()
3 , Effects_()
4 , loader
5 , loaderFn
6 ) where
7
8import Prelude (Unit(), ($), (>>=), (<$>), (<*>), (++), (<<<), bind, const, id, pure, unit)
9
10import Control.Bind (join)
11import Control.Monad.Eff (Eff(), foreachE)
12import Control.Monad.Eff.Console (CONSOLE())
13import Control.Monad.Eff.Exception (EXCEPTION(), Error(), error)
14
15import Data.Array ((!!))
16import Data.Either (Either(..), either)
17import Data.Function (Fn2(), mkFn2)
18import Data.Maybe (maybe)
19import Data.Nullable (toMaybe)
20import Data.String.Regex (Regex(), match, noFlags, regex)
21
22import Unsafe.Coerce (unsafeCoerce)
23
24import PursLoader.Debug (debug)
25import PursLoader.JsStringEscape (jsStringEscape)
26import PursLoader.LoaderRef
27 ( AsyncCallback()
28 , LoaderRef()
29 , Loader()
30 , async
31 , cacheable
32 , addDependency
33 , resourcePath
34 )
35import PursLoader.Path (dirname, joinPath, relative)
36import PursLoader.Plugin as Plugin
37
38type Effects eff = (console :: CONSOLE, err :: EXCEPTION | eff)
39
40type Effects_ eff = Effects (loader :: Loader | eff)
41
42loader :: forall eff. LoaderRef -> String -> Eff (Effects_ eff) Unit
43loader ref source = do
44 callback <- async ref
45
46 cacheable ref
47
48 debug "Invoke PureScript plugin compilation"
49
50 pluginContext.compile (compile callback)
51 where
52 pluginContext :: Plugin.Context (Effects_ eff)
53 pluginContext = (unsafeCoerce ref).purescriptWebpackPluginContext
54
55 compile :: AsyncCallback (Effects eff) -> Plugin.Compile (Effects_ eff)
56 compile callback error' graph = do
57 either (const $ pure unit) (\a -> debug ("Adding PureScript dependency " ++ a)) name
58
59 addDependency ref (resourcePath ref)
60
61 either (const $ callback (pure fixedError) "") id
62 (handle <$> name <*> dependencies <*> exports)
63 where
64 fixedError :: Error
65 fixedError = error "PureScript compilation has failed."
66
67 handle :: String -> Array String -> String -> Eff (Effects_ eff) Unit
68 handle name' deps res = do
69 debug ("Adding PureScript dependencies for " ++ name')
70 foreachE deps (addDependency ref)
71 debug "Generated loader result"
72 debug res
73 callback (const fixedError <$> toMaybe error') res
74
75 exports :: Either Error String
76 exports =
77 if pluginContext.options.bundle
78 then bundleExport <$> name
79 else moduleExport <<< modulePath <$> name
80 where
81 bundleExport :: String -> String
82 bundleExport name' = "module.exports = require('" ++ jsStringEscape path ++ "')['" ++ name' ++ "'];"
83 where
84 path :: String
85 path = relative resourceDir pluginContext.options.bundleOutput
86
87 moduleExport :: String -> String
88 moduleExport path = "module.exports = require('" ++ jsStringEscape path ++ "');"
89
90 modulePath :: String -> String
91 modulePath = relative resourceDir <<< joinPath pluginContext.options.output
92
93 resourceDir :: String
94 resourceDir = dirname (resourcePath ref)
95
96 dependencies :: Either Error (Array String)
97 dependencies = Plugin.dependenciesOf graph (resourcePath ref)
98
99 name :: Either Error String
100 name =
101 maybe (Left $ error "Failed to parse module name") Right
102 (join $ match re source >>= \as -> as !! 1)
103 where
104 re :: Regex
105 re = regex "(?:^|\\n)module\\s+([\\w\\.]+)" noFlags { ignoreCase = true }
106
107loaderFn :: forall eff. Fn2 LoaderRef String (Eff (Effects_ eff) Unit)
108loaderFn = 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 @@
1'use strict';
2
3// module PursLoader.LoaderRef
4
5function asyncFn(isJust, fromMaybe, ref){
6 return function(){
7 var callback = ref.async();
8 return function(error){
9 return function(value){
10 return function(){
11 return isJust(error) ? callback(fromMaybe(new Error())(error))
12 : callback(null, value);
13 };
14 };
15 };
16 };
17}
18function cacheable(ref){
19 return function(){
20 return ref.cacheable && ref.cacheable();
21 };
22}
23
24function clearDependencies(ref){
25 return function(){
26 return ref.clearDependencies();
27 };
28}
29
30function resourcePath(ref){
31 return ref.resourcePath;
32}
33
34function addDependency(ref){
35 return function(dep){
36 return function(){
37 return ref.addDependency(dep);
38 };
39 };
40}
41
42exports.asyncFn = asyncFn;
43
44exports.cacheable = cacheable;
45
46exports.clearDependencies = clearDependencies;
47
48exports.resourcePath = resourcePath;
49
50exports.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 @@
1module PursLoader.LoaderRef
2 ( LoaderRef()
3 , Loader()
4 , AsyncCallback()
5 , async
6 , cacheable
7 , clearDependencies
8 , addDependency
9 , resourcePath
10 ) where
11
12import Prelude (Unit())
13
14import Control.Monad.Eff (Eff())
15import Control.Monad.Eff.Exception (Error())
16
17import Data.Function (Fn3(), runFn3)
18import Data.Maybe (Maybe(), fromMaybe, isJust)
19
20type AsyncCallback eff = Maybe Error -> String -> Eff (loader :: Loader | eff) Unit
21
22data LoaderRef
23
24foreign import data Loader :: !
25
26foreign import asyncFn :: forall eff. Fn3 (Maybe Error -> Boolean)
27 (Error -> Maybe Error -> Error)
28 LoaderRef
29 (Eff (loader :: Loader | eff) (AsyncCallback eff))
30
31async :: forall eff. LoaderRef -> Eff (loader :: Loader | eff) (Maybe Error -> String -> Eff (loader :: Loader | eff) Unit)
32async ref = runFn3 asyncFn isJust fromMaybe ref
33
34foreign import cacheable :: forall eff. LoaderRef -> Eff (loader :: Loader | eff) Unit
35
36foreign import clearDependencies :: forall eff. LoaderRef -> Eff (loader :: Loader | eff) Unit
37
38foreign import resourcePath :: LoaderRef -> String
39
40foreign 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 @@
1'use strict'
2
3// module PursLoader.Path
4
5var path = require('path');
6
7function relative(from) {
8 return function(to){
9 return path.relative(from, to);
10 };
11}
12exports.relative = relative;
13
14
15function joinPath(a) {
16 return function(b) {
17 return path.join(a, b);
18 };
19}
20exports.joinPath = joinPath;
21
22exports.resolve = path.resolve;
23
24exports.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 @@
1module PursLoader.Path
2 ( relative
3 , resolve
4 , dirname
5 , joinPath
6 ) where
7
8foreign import relative :: String -> String -> String
9
10foreign import resolve :: String -> String
11
12foreign import dirname :: String -> String
13
14foreign 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 @@
1'use strict';
2
3// module PursLoader.Plugin
4
5function dependenciesOfFn(left, right, graph, node) {
6 try {
7 var dependencies = graph.dependenciesOf(node);
8 return right(dependencies);
9 }
10 catch (error) {
11 return left(error);
12 }
13}
14exports.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 @@
1module PursLoader.Plugin
2 ( Compile()
3 , Context()
4 , Options()
5 , DependencyGraph()
6 , dependenciesOf
7 ) where
8
9import Prelude (Unit())
10
11import Control.Monad.Eff (Eff())
12import Control.Monad.Eff.Exception (Error())
13
14import Data.Either (Either(..))
15import Data.Function (Fn4(), runFn4)
16import Data.Nullable (Nullable())
17
18type Compile eff = Nullable Error -> DependencyGraph -> Eff eff Unit
19
20type Context eff = { compile :: Compile eff -> Eff eff Unit, options :: Options }
21
22type Options = { bundle :: Boolean, output :: String, bundleOutput :: String }
23
24dependenciesOf :: DependencyGraph -> String -> Either Error (Array String)
25dependenciesOf = runFn4 dependenciesOfFn Left Right
26
27foreign import data DependencyGraph :: *
28
29foreign import dependenciesOfFn
30 :: Fn4 (Error -> Either Error (Array String))
31 (Array String -> Either Error (Array String))
32 DependencyGraph
33 String
34 (Either Error (Array String))
diff --git a/src/index.js b/src/index.js
new file mode 100644
index 0000000..72da4d0
--- /dev/null
+++ b/src/index.js
@@ -0,0 +1,481 @@
1'use strict'
2
3const colors = require('chalk')
4const debug = require('debug')('purs-loader')
5const loaderUtils = require('loader-utils')
6const globby = require('globby')
7const Promise = require('bluebird')
8const fs = Promise.promisifyAll(require('fs'))
9const spawn = require('child_process').spawn
10const path = require('path')
11const retryPromise = require('promise-retry')
12
13const ffiModuleRegex = /\/\/\s+module\s+([\w\.]+)/i
14const srcModuleRegex = /(?:^|\n)module\s+([\w\.]+)/i
15const requireRegex = /require\(['"]\.\.\/([\w\.]+)['"]\)/g
16
17module.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 || {}
22
23 const options = Object.assign({
24 context: config.context,
25 psc: 'psc',
26 pscArgs: {},
27 pscBundle: 'psc-bundle',
28 pscBundleArgs: {},
29 pscIde: false,
30 pscIdeColors: webpackOptions.psc === 'psa' || query.psc === 'psa',
31 pscIdeArgs: {},
32 bundleOutput: 'output/bundle.js',
33 bundleNamespace: 'PS',
34 bundle: false,
35 warnings: true,
36 output: 'output',
37 src: [
38 path.join('src', '**', '*.purs'),
39 path.join('bower_components', 'purescript-*', 'src', '**', '*.purs')
40 ],
41 ffi: [
42 path.join('src', '**', '*.js'),
43 path.join('bower_components', 'purescript-*', 'src', '**', '*.js')
44 ],
45 }, webpackOptions, query)
46
47 this.cacheable && this.cacheable()
48
49 let cache = config.purescriptLoaderCache = config.purescriptLoaderCache || {
50 rebuild: false,
51 deferred: [],
52 bundleModules: [],
53 }
54
55 if (!config.purescriptLoaderInstalled) {
56 config.purescriptLoaderInstalled = true
57
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,
62 deferred: [],
63 ideServer: cache.ideServer
64 }
65 })
66
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}`)
71 }
72
73 if (cache.errors) {
74 compilation.errors.unshift(`PureScript compilation:\n${cache.errors}`)
75 }
76
77 callback()
78 })
79 }
80
81 const psModuleName = match(srcModuleRegex, source)
82 const psModule = {
83 name: psModuleName,
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')),
89 options: options,
90 cache: cache,
91 }
92
93 debug('loader called', psModule.name)
94
95 if (options.bundle) {
96 cache.bundleModules.push(psModule.name)
97 }
98
99 if (cache.rebuild) {
100 return connectIdeServer(psModule)
101 .then(rebuild)
102 .then(toJavaScript)
103 .then(psModule.load)
104 .catch(psModule.reject)
105 }
106
107 if (cache.compilationFinished) {
108 return toJavaScript(psModule).then(psModule.load).catch(psModule.reject)
109 }
110
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)
114
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)
120 }))
121 .catch(error => {
122 cache.deferred[0].reject(error)
123 cache.deferred.slice(1).forEach(psModule => psModule.reject(true))
124 })
125 }
126}
127
128// The actual loader is executed *after* purescript compilation.
129function 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
134
135 debug('loading JavaScript for', psModule.name)
136
137 return Promise.props({
138 js: fs.readFileAsync(jsPath, 'utf8'),
139 psModuleMap: psModuleMap(options, cache)
140 }).then(result => {
141 let js = ''
142
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 + '"]'
148 } else {
149 // replace require paths to output files generated by psc with paths
150 // to purescript sources, which are then also run through this loader.
151 js = result.js
152 .replace(requireRegex, (m, p1) => {
153 return 'require("' + result.psModuleMap[p1].src + '")'
154 })
155 .replace(/require\(['"]\.\/foreign['"]\)/g, (m, p1) => {
156 return 'require("' + result.psModuleMap[psModule.name].ffi + '")'
157 })
158 }
159
160 return js
161 })
162}
163
164function compile(psModule) {
165 const options = psModule.options
166 const cache = psModule.cache
167 const stderr = []
168
169 if (cache.compilationStarted) return Promise.resolve(psModule)
170
171 cache.compilationStarted = true
172
173 const args = dargs(Object.assign({
174 _: options.src,
175 ffi: options.ffi,
176 output: options.output,
177 }, options.pscArgs))
178
179 debug('spawning compiler %s %o', options.psc, args)
180
181 return (new Promise((resolve, reject) => {
182 console.log('\nCompiling PureScript...')
183
184 const compilation = spawn(options.psc, args)
185
186 compilation.stdout.on('data', data => stderr.push(data.toString()))
187 compilation.stderr.on('data', data => stderr.push(data.toString()))
188
189 compilation.on('close', code => {
190 console.log('Finished compiling PureScript.')
191 cache.compilationFinished = true
192 if (code !== 0) {
193 cache.errors = stderr.join('')
194 reject(true)
195 } else {
196 cache.warnings = stderr.join('')
197 resolve(psModule)
198 }
199 })
200 }))
201 .then(compilerOutput => {
202 if (options.bundle) {
203 return bundle(options, cache).then(() => psModule)
204 }
205 return psModule
206 })
207}
208
209function rebuild(psModule) {
210 const options = psModule.options
211 const cache = psModule.cache
212
213 debug('attempting rebuild with psc-ide-client %s', psModule.srcPath)
214
215 const request = (body) => new Promise((resolve, reject) => {
216 const args = dargs(options.pscIdeArgs)
217 const ideClient = spawn('psc-ide-client', args)
218
219 ideClient.stdout.once('data', data => {
220 let res = null
221
222 try {
223 res = JSON.parse(data.toString())
224 debug(res)
225 } catch (err) {
226 return reject(err)
227 }
228
229 if (res && !Array.isArray(res.result)) {
230 return res.resultType === 'success'
231 ? resolve(psModule)
232 : reject('psc-ide rebuild failed')
233 }
234
235 Promise.map(res.result, (item, i) => {
236 debug(item)
237 return formatIdeResult(item, options, i, res.result.length)
238 })
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' }))
245 .then(resolve)
246 .catch(() => reject('psc-ide rebuild failed'))
247 }
248 cache.errors = compileMessages.join('\n')
249 reject('psc-ide rebuild failed')
250 } else {
251 cache.warnings = compileMessages.join('\n')
252 resolve(psModule)
253 }
254 })
255 })
256
257 ideClient.stderr.once('data', data => reject(data.toString()))
258
259 ideClient.stdin.write(JSON.stringify(body))
260 ideClient.stdin.write('\n')
261 })
262
263 return request({
264 command: 'rebuild',
265 params: {
266 file: psModule.srcPath,
267 }
268 })
269}
270
271function 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
277
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)
284
285 if (endsOnNewline) {
286 lines.splice(lines.length - 1, 1)
287 pos.endLine = pos.endLine - 1
288 pos.endColumn = lines[lines.length - 1].length || 1
289 }
290
291 // strip newlines at the end
292 if (endsOnNewline) {
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
298 trimmed.push(line)
299 }
300 return trimmed
301 }, []).reverse()
302 pos.endLine = pos.endLine - (lines.length - trimmed.length)
303 pos.endColumn = trimmed[trimmed.length - 1].length || 1
304 }
305
306 const spaces = ' '.repeat(String(pos.endLine).length)
307 let snippet = trimmed.map((line, i) => {
308 return ` ${pos.startLine + i} ${line}`
309 }).join('\n')
310
311 if (trimmed.length === 1) {
312 snippet += `\n ${spaces} ${' '.repeat(pos.startColumn - 1)}${up.repeat(pos.endColumn - pos.startColumn + 1)}`
313 } else {
314 snippet = ` ${spaces} ${' '.repeat(pos.startColumn - 1)}${down}\n${snippet}`
315 snippet += `\n ${spaces} ${' '.repeat(pos.endColumn - 1)}${up}`
316 }
317
318 return Promise.resolve(
319 `\n${numAndErr} ${fileAndPos}\n\n${snippet}\n\n${result.message}`
320 )
321 })
322}
323
324function bundle(options, cache) {
325 if (cache.bundle) return Promise.resolve(cache.bundle)
326
327 const stdout = []
328 const stderr = cache.bundle = []
329
330 const args = dargs(Object.assign({
331 _: [path.join(options.output, '*', '*.js')],
332 output: options.bundleOutput,
333 namespace: options.bundleNamespace,
334 }, options.pscBundleArgs))
335
336 cache.bundleModules.forEach(name => args.push('--module', name))
337
338 debug('spawning bundler %s %o', options.pscBundle, args.join(' '))
339
340 return (new Promise((resolve, reject) => {
341 console.log('Bundling PureScript...')
342
343 const compilation = spawn(options.pscBundle, args)
344
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 => {
348 if (code !== 0) {
349 cache.errors = (cache.errors || '') + stderr.join('')
350 return reject(true)
351 }
352 cache.bundle = stderr
353 resolve(fs.appendFileAsync('output/bundle.js', `module.exports = ${options.bundleNamespace}`))
354 })
355 }))
356}
357
358// map of PS module names to their source path
359function psModuleMap(options, cache) {
360 if (cache.psModuleMap) return Promise.resolve(cache.psModuleMap)
361
362 const globs = [].concat(options.src).concat(options.ffi)
363
364 return globby(globs).then(paths => {
365 return Promise
366 .props(paths.reduce((map, file) => {
367 map[file] = fs.readFileAsync(file, 'utf8')
368 return map
369 }, {}))
370 .then(fileMap => {
371 cache.psModuleMap = Object.keys(fileMap).reduce((map, file) => {
372 const source = fileMap[file]
373 const ext = path.extname(file)
374 const isPurs = ext.match(/purs$/i)
375 const moduleRegex = isPurs ? srcModuleRegex : ffiModuleRegex
376 const moduleName = match(moduleRegex, source)
377 map[moduleName] = map[moduleName] || {}
378 if (isPurs) {
379 map[moduleName].src = path.resolve(file)
380 } else {
381 map[moduleName].ffi = path.resolve(file)
382 }
383 return map
384 }, {})
385 return cache.psModuleMap
386 })
387 })
388}
389
390function connectIdeServer(psModule) {
391 const options = psModule.options
392 const cache = psModule.cache
393
394 if (cache.ideServer) return Promise.resolve(psModule)
395
396 cache.ideServer = true
397
398 const connect = () => new Promise((resolve, reject) => {
399 const args = dargs(options.pscIdeArgs)
400
401 debug('attempting to connect to psc-ide-server', args)
402
403 const ideClient = spawn('psc-ide-client', args)
404
405 ideClient.stderr.on('data', data => {
406 debug(data.toString())
407 cache.ideServer = false
408 reject(true)
409 })
410 ideClient.stdout.once('data', data => {
411 debug(data.toString())
412 if (data.toString()[0] === '{') {
413 const res = JSON.parse(data.toString())
414 if (res.resultType === 'success') {
415 cache.ideServer = ideServer
416 resolve(psModule)
417 } else {
418 cache.ideServer = ideServer
419 reject(true)
420 }
421 } else {
422 cache.ideServer = false
423 reject(true)
424 }
425 })
426 ideClient.stdin.resume()
427 ideClient.stdin.write(JSON.stringify({ command: 'load' }))
428 ideClient.stdin.write('\n')
429 })
430
431 const args = dargs(Object.assign({
432 outputDirectory: options.output,
433 }, options.pscIdeArgs))
434
435 debug('attempting to start psc-ide-server', args)
436
437 const ideServer = cache.ideServer = spawn('psc-ide-server', [])
438 ideServer.stderr.on('data', data => {
439 debug(data.toString())
440 })
441
442 return retryPromise((retry, number) => {
443 return connect().catch(error => {
444 if (!cache.ideServer && number === 9) {
445 debug(error)
446
447 console.log(
448 'failed to connect to or start psc-ide-server, ' +
449 'full compilation will occur on rebuild'
450 )
451
452 return Promise.resolve(psModule)
453 }
454
455 return retry(error)
456 })
457 }, {
458 retries: 9,
459 factor: 1,
460 minTimeout: 333,
461 maxTimeout: 333,
462 })
463}
464
465function match(regex, str) {
466 const matches = str.match(regex)
467 return matches && matches[1]
468}
469
470function dargs(obj) {
471 return Object.keys(obj).reduce((args, key) => {
472 const arg = '--' + key.replace(/[A-Z]/g, '-$&').toLowerCase();
473 const val = obj[key]
474
475 if (key === '_') val.forEach(v => args.push(v))
476 else if (Array.isArray(val)) val.forEach(v => args.push(arg, v))
477 else args.push(arg, obj[key])
478
479 return args.filter(arg => (typeof arg !== 'boolean'))
480 }, [])
481}