aboutsummaryrefslogtreecommitdiffhomepage
path: root/src
diff options
context:
space:
mode:
authoreric <thul.eric@gmail.com>2016-06-12 16:06:34 -0400
committerGitHub <noreply@github.com>2016-06-12 16:06:34 -0400
commit27638f63256b6d38745eef251b3327536766e7c3 (patch)
tree75e135abd95c9fee60ef9d9ba7d35440fd2c1ddf /src
parent7243be70a2163be2230a5f2739768137305a24ef (diff)
parent0b853815ef14d35cedebc2c7806fd2f9ff9d5ab5 (diff)
downloadpurs-loader-27638f63256b6d38745eef251b3327536766e7c3.tar.gz
purs-loader-27638f63256b6d38745eef251b3327536766e7c3.tar.zst
purs-loader-27638f63256b6d38745eef251b3327536766e7c3.zip
Merge pull request #61 from ethul/topic/issues
Reduce building of PureScript module map
Diffstat (limited to 'src')
-rw-r--r--src/PsModuleMap.js66
-rw-r--r--src/Psc.js92
-rw-r--r--src/PscIde.js237
-rw-r--r--src/dargs.js16
-rw-r--r--src/index.js396
5 files changed, 452 insertions, 355 deletions
diff --git a/src/PsModuleMap.js b/src/PsModuleMap.js
new file mode 100644
index 0000000..2193f02
--- /dev/null
+++ b/src/PsModuleMap.js
@@ -0,0 +1,66 @@
1'use strict';
2
3const path = require('path');
4
5const Promise = require('bluebird');
6
7const fs = Promise.promisifyAll(require('fs'));
8
9const globby = require('globby');
10
11const debug = require('debug')('purs-loader')
12
13const srcModuleRegex = /(?:^|\n)module\s+([\w\.]+)/i
14
15function match(str) {
16 const matches = str.match(srcModuleRegex);
17 return matches && matches[1];
18}
19module.exports.match = match;
20
21function makeMapEntry(filePurs) {
22 const dirname = path.dirname(filePurs);
23
24 const basename = path.basename(filePurs, '.purs');
25
26 const fileJs = path.join(dirname, `${basename}.js`);
27
28 const result = Promise.props({
29 filePurs: fs.readFileAsync(filePurs, 'utf8'),
30 fileJs: fs.readFileAsync(fileJs, 'utf8').catch(() => undefined)
31 }).then(fileMap => {
32 const sourcePurs = fileMap.filePurs;
33
34 const sourceJs = fileMap.fileJs;
35
36 const moduleName = match(sourcePurs);
37
38 const map = {};
39
40 map[moduleName] = map[moduleName] || {};
41
42 map[moduleName].src = path.resolve(filePurs);
43
44 if (sourceJs) {
45 map[moduleName].ffi = path.resolve(fileJs);
46 }
47
48 return map;
49 });
50
51 return result;
52}
53module.exports.makeMapEntry = makeMapEntry;
54
55function makeMap(src) {
56 debug('loading PureScript source and FFI files from %o', src);
57
58 const globs = [].concat(src);
59
60 return globby(globs).then(paths =>
61 Promise.all(paths.map(makeMapEntry)).then(result =>
62 result.reduce(Object.assign, {})
63 )
64 );
65}
66module.exports.makeMap = makeMap;
diff --git a/src/Psc.js b/src/Psc.js
new file mode 100644
index 0000000..9269e0f
--- /dev/null
+++ b/src/Psc.js
@@ -0,0 +1,92 @@
1'use strict';
2
3const path = require('path');
4
5const Promise = require('bluebird')
6
7const fs = Promise.promisifyAll(require('fs'))
8
9const spawn = require('cross-spawn')
10
11const debug = require('debug')('purs-loader');
12
13const dargs = require('./dargs');
14
15function compile(psModule) {
16 const options = psModule.options
17 const cache = psModule.cache
18 const stderr = []
19
20 if (cache.compilationStarted) return Promise.resolve(psModule)
21
22 cache.compilationStarted = true
23
24 const args = dargs(Object.assign({
25 _: options.src,
26 output: options.output,
27 }, options.pscArgs))
28
29 debug('spawning compiler %s %o', options.psc, args)
30
31 return (new Promise((resolve, reject) => {
32 console.log('\nCompiling PureScript...')
33
34 const compilation = spawn(options.psc, args)
35
36 compilation.stdout.on('data', data => stderr.push(data.toString()))
37 compilation.stderr.on('data', data => stderr.push(data.toString()))
38
39 compilation.on('close', code => {
40 console.log('Finished compiling PureScript.')
41 cache.compilationFinished = true
42 if (code !== 0) {
43 cache.errors = stderr.join('')
44 reject(true)
45 } else {
46 cache.warnings = stderr.join('')
47 resolve(psModule)
48 }
49 })
50 }))
51 .then(compilerOutput => {
52 if (options.bundle) {
53 return bundle(options, cache).then(() => psModule)
54 }
55 return psModule
56 })
57}
58module.exports.compile = compile;
59
60function bundle(options, cache) {
61 if (cache.bundle) return Promise.resolve(cache.bundle)
62
63 const stdout = []
64 const stderr = cache.bundle = []
65
66 const args = dargs(Object.assign({
67 _: [path.join(options.output, '*', '*.js')],
68 output: options.bundleOutput,
69 namespace: options.bundleNamespace,
70 }, options.pscBundleArgs))
71
72 cache.bundleModules.forEach(name => args.push('--module', name))
73
74 debug('spawning bundler %s %o', options.pscBundle, args.join(' '))
75
76 return (new Promise((resolve, reject) => {
77 console.log('Bundling PureScript...')
78
79 const compilation = spawn(options.pscBundle, args)
80
81 compilation.stdout.on('data', data => stdout.push(data.toString()))
82 compilation.stderr.on('data', data => stderr.push(data.toString()))
83 compilation.on('close', code => {
84 if (code !== 0) {
85 cache.errors = (cache.errors || '') + stderr.join('')
86 return reject(true)
87 }
88 cache.bundle = stderr
89 resolve(fs.appendFileAsync('output/bundle.js', `module.exports = ${options.bundleNamespace}`))
90 })
91 }))
92}
diff --git a/src/PscIde.js b/src/PscIde.js
new file mode 100644
index 0000000..9d6c1ff
--- /dev/null
+++ b/src/PscIde.js
@@ -0,0 +1,237 @@
1'use strict';
2
3const path = require('path');
4
5const Promise = require('bluebird');
6
7const fs = Promise.promisifyAll(require('fs'));
8
9const retryPromise = require('promise-retry');
10
11const spawn = require('cross-spawn');
12
13const colors = require('chalk');
14
15const debug = require('debug')('purs-loader');
16
17const dargs = require('./dargs');
18
19const Psc = require('./Psc');
20
21const PsModuleMap = require('./PsModuleMap');
22
23function connect(psModule) {
24 const options = psModule.options
25 const cache = psModule.cache
26
27 if (cache.ideServer) return Promise.resolve(psModule)
28
29 cache.ideServer = true
30
31 const connect_ = () => new Promise((resolve, reject) => {
32 const args = dargs(options.pscIdeArgs)
33
34 debug('attempting to connect to psc-ide-server', args)
35
36 const ideClient = spawn('psc-ide-client', args)
37
38 ideClient.stderr.on('data', data => {
39 debug(data.toString())
40 cache.ideServer = false
41 reject(true)
42 })
43 ideClient.stdout.once('data', data => {
44 debug(data.toString())
45 if (data.toString()[0] === '{') {
46 const res = JSON.parse(data.toString())
47 if (res.resultType === 'success') {
48 cache.ideServer = ideServer
49 resolve(psModule)
50 } else {
51 cache.ideServer = ideServer
52 reject(true)
53 }
54 } else {
55 cache.ideServer = false
56 reject(true)
57 }
58 })
59 ideClient.stdin.resume()
60 ideClient.stdin.write(JSON.stringify({ command: 'load' }))
61 ideClient.stdin.write('\n')
62 })
63
64 const args = dargs(Object.assign({
65 outputDirectory: options.output,
66 }, options.pscIdeArgs))
67
68 debug('attempting to start psc-ide-server', args)
69
70 const ideServer = cache.ideServer = spawn('psc-ide-server', [])
71 ideServer.stderr.on('data', data => {
72 debug(data.toString())
73 })
74
75 return retryPromise((retry, number) => {
76 return connect_().catch(error => {
77 if (!cache.ideServer && number === 9) {
78 debug(error)
79
80 console.log(
81 'failed to connect to or start psc-ide-server, ' +
82 'full compilation will occur on rebuild'
83 )
84
85 return Promise.resolve(psModule)
86 }
87
88 return retry(error)
89 })
90 }, {
91 retries: 9,
92 factor: 1,
93 minTimeout: 333,
94 maxTimeout: 333,
95 })
96}
97module.exports.connect = connect;
98
99function rebuild(psModule) {
100 const options = psModule.options
101 const cache = psModule.cache
102
103 debug('attempting rebuild with psc-ide-client %s', psModule.srcPath)
104
105 const request = (body) => new Promise((resolve, reject) => {
106 const args = dargs(options.pscIdeArgs)
107 const ideClient = spawn('psc-ide-client', args)
108
109 var stdout = ''
110 var stderr = ''
111
112 ideClient.stdout.on('data', data => {
113 stdout = stdout + data.toString()
114 })
115
116 ideClient.stderr.on('data', data => {
117 stderr = stderr + data.toString()
118 })
119
120 ideClient.on('close', code => {
121 if (code !== 0) {
122 const error = stderr === '' ? 'Failed to spawn psc-ide-client' : stderr
123 return reject(new Error(error))
124 }
125
126 let res = null
127
128 try {
129 res = JSON.parse(stdout.toString())
130 debug(res)
131 } catch (err) {
132 return reject(err)
133 }
134
135 if (res && !Array.isArray(res.result)) {
136 return res.resultType === 'success'
137 ? resolve(psModule)
138 : reject('psc-ide rebuild failed')
139 }
140
141 Promise.map(res.result, (item, i) => {
142 debug(item)
143 return formatIdeResult(item, options, i, res.result.length)
144 })
145 .then(compileMessages => {
146 if (res.resultType === 'error') {
147 if (res.result.some(item => item.errorCode === 'UnknownModule' || item.errorCode === 'UnknownName')) {
148 debug('unknown module, attempting full recompile')
149 return Psc.compile(psModule)
150 .then(() => PsModuleMap.makeMap(options.src).then(map => {
151 debug('rebuilt module map');
152 cache.psModuleMap = map;
153 }))
154 .then(() => request({ command: 'load' }))
155 .then(resolve)
156 .catch(() => reject('psc-ide rebuild failed'))
157 }
158 cache.errors = compileMessages.join('\n')
159 reject('psc-ide rebuild failed')
160 } else {
161 cache.warnings = compileMessages.join('\n')
162 resolve(psModule)
163 }
164 })
165 })
166
167 ideClient.stdin.write(JSON.stringify(body))
168 ideClient.stdin.write('\n')
169 })
170
171 return request({
172 command: 'rebuild',
173 params: {
174 file: psModule.srcPath,
175 }
176 })
177}
178module.exports.rebuild = rebuild;
179
180function formatIdeResult(result, options, index, length) {
181 let numAndErr = `[${index+1}/${length} ${result.errorCode}]`
182 numAndErr = options.pscIdeColors ? colors.yellow(numAndErr) : numAndErr
183
184 function makeResult() {
185 return Promise.resolve(`\n${numAndErr} ${result.message}`)
186 }
187
188 function makeResultSnippet(filename, pos) {
189 const srcPath = path.relative(options.context, filename);
190 const fileAndPos = `${srcPath}:${pos.startLine}:${pos.startColumn}`
191
192 return fs.readFileAsync(filename, 'utf8').then(source => {
193 const lines = source.split('\n').slice(pos.startLine - 1, pos.endLine)
194 const endsOnNewline = pos.endColumn === 1 && pos.startLine !== pos.endLine
195 const up = options.pscIdeColors ? colors.red('^') : '^'
196 const down = options.pscIdeColors ? colors.red('v') : 'v'
197 let trimmed = lines.slice(0)
198
199 if (endsOnNewline) {
200 lines.splice(lines.length - 1, 1)
201 pos.endLine = pos.endLine - 1
202 pos.endColumn = lines[lines.length - 1].length || 1
203 }
204
205 // strip newlines at the end
206 if (endsOnNewline) {
207 trimmed = lines.reverse().reduce((trimmed, line, i) => {
208 if (i === 0 && line === '') trimmed.trimming = true
209 if (!trimmed.trimming) trimmed.push(line)
210 if (trimmed.trimming && line !== '') {
211 trimmed.trimming = false
212 trimmed.push(line)
213 }
214 return trimmed
215 }, []).reverse()
216 pos.endLine = pos.endLine - (lines.length - trimmed.length)
217 pos.endColumn = trimmed[trimmed.length - 1].length || 1
218 }
219
220 const spaces = ' '.repeat(String(pos.endLine).length)
221 let snippet = trimmed.map((line, i) => {
222 return ` ${pos.startLine + i} ${line}`
223 }).join('\n')
224
225 if (trimmed.length === 1) {
226 snippet += `\n ${spaces} ${' '.repeat(pos.startColumn - 1)}${up.repeat(pos.endColumn - pos.startColumn + 1)}`
227 } else {
228 snippet = ` ${spaces} ${' '.repeat(pos.startColumn - 1)}${down}\n${snippet}`
229 snippet += `\n ${spaces} ${' '.repeat(pos.endColumn - 1)}${up}`
230 }
231
232 return Promise.resolve(`\n${numAndErr} ${fileAndPos}\n\n${snippet}\n\n${result.message}`)
233 })
234 }
235
236 return result.filename && result.position ? makeResultSnippet(result.filename, result.position) : makeResult();
237}
diff --git a/src/dargs.js b/src/dargs.js
new file mode 100644
index 0000000..e5c574c
--- /dev/null
+++ b/src/dargs.js
@@ -0,0 +1,16 @@
1'use strict';
2
3function dargs(obj) {
4 return Object.keys(obj).reduce((args, key) => {
5 const arg = '--' + key.replace(/[A-Z]/g, '-$&').toLowerCase();
6 const val = obj[key]
7
8 if (key === '_') val.forEach(v => args.push(v))
9 else if (Array.isArray(val)) val.forEach(v => args.push(arg, v))
10 else args.push(arg, obj[key])
11
12 return args.filter(arg => (typeof arg !== 'boolean'))
13 }, [])
14}
15
16module.exports = dargs;
diff --git a/src/index.js b/src/index.js
index cfba1e2..c73fdd5 100644
--- a/src/index.js
+++ b/src/index.js
@@ -1,17 +1,16 @@
1'use strict' 1'use strict'
2 2
3const colors = require('chalk')
4const debug = require('debug')('purs-loader') 3const debug = require('debug')('purs-loader')
5const loaderUtils = require('loader-utils') 4const loaderUtils = require('loader-utils')
6const globby = require('globby')
7const Promise = require('bluebird') 5const Promise = require('bluebird')
8const fs = Promise.promisifyAll(require('fs')) 6const fs = Promise.promisifyAll(require('fs'))
9const spawn = require('cross-spawn')
10const path = require('path') 7const path = require('path')
11const retryPromise = require('promise-retry')
12const jsStringEscape = require('js-string-escape') 8const jsStringEscape = require('js-string-escape')
9const PsModuleMap = require('./PsModuleMap');
10const Psc = require('./Psc');
11const PscIde = require('./PscIde');
12const dargs = require('./dargs');
13 13
14const srcModuleRegex = /(?:^|\n)module\s+([\w\.]+)/i
15const requireRegex = /require\(['"]\.\.\/([\w\.]+)['"]\)/g 14const requireRegex = /require\(['"]\.\.\/([\w\.]+)['"]\)/g
16 15
17module.exports = function purescriptLoader(source, map) { 16module.exports = function purescriptLoader(source, map) {
@@ -45,7 +44,7 @@ module.exports = function purescriptLoader(source, map) {
45 let cache = config.purescriptLoaderCache = config.purescriptLoaderCache || { 44 let cache = config.purescriptLoaderCache = config.purescriptLoaderCache || {
46 rebuild: false, 45 rebuild: false,
47 deferred: [], 46 deferred: [],
48 bundleModules: [], 47 bundleModules: []
49 } 48 }
50 49
51 if (!config.purescriptLoaderInstalled) { 50 if (!config.purescriptLoaderInstalled) {
@@ -53,10 +52,14 @@ module.exports = function purescriptLoader(source, map) {
53 52
54 // invalidate loader cache when bundle is marked as invalid (in watch mode) 53 // invalidate loader cache when bundle is marked as invalid (in watch mode)
55 this._compiler.plugin('invalid', () => { 54 this._compiler.plugin('invalid', () => {
55 debug('invalidating loader cache');
56
56 cache = config.purescriptLoaderCache = { 57 cache = config.purescriptLoaderCache = {
57 rebuild: options.pscIde, 58 rebuild: options.pscIde,
58 deferred: [], 59 deferred: [],
59 ideServer: cache.ideServer 60 bundleModules: [],
61 ideServer: cache.ideServer,
62 psModuleMap: cache.psModuleMap
60 } 63 }
61 }) 64 })
62 65
@@ -64,17 +67,19 @@ module.exports = function purescriptLoader(source, map) {
64 this._compiler.plugin('after-compile', (compilation, callback) => { 67 this._compiler.plugin('after-compile', (compilation, callback) => {
65 if (options.warnings && cache.warnings) { 68 if (options.warnings && cache.warnings) {
66 compilation.warnings.unshift(`PureScript compilation:\n${cache.warnings}`) 69 compilation.warnings.unshift(`PureScript compilation:\n${cache.warnings}`)
70 cache.warnings = null;
67 } 71 }
68 72
69 if (cache.errors) { 73 if (cache.errors) {
70 compilation.errors.unshift(`PureScript compilation:\n${cache.errors}`) 74 compilation.errors.unshift(`PureScript compilation:\n${cache.errors}`)
75 cache.errors = null;
71 } 76 }
72 77
73 callback() 78 callback()
74 }) 79 })
75 } 80 }
76 81
77 const psModuleName = match(srcModuleRegex, source) 82 const psModuleName = PsModuleMap.match(source)
78 const psModule = { 83 const psModule = {
79 name: psModuleName, 84 name: psModuleName,
80 load: js => callback(null, js), 85 load: js => callback(null, js),
@@ -93,8 +98,8 @@ module.exports = function purescriptLoader(source, map) {
93 } 98 }
94 99
95 if (cache.rebuild) { 100 if (cache.rebuild) {
96 return connectIdeServer(psModule) 101 return PscIde.connect(psModule)
97 .then(rebuild) 102 .then(PscIde.rebuild)
98 .then(toJavaScript) 103 .then(toJavaScript)
99 .then(psModule.load) 104 .then(psModule.load)
100 .catch(psModule.reject) 105 .catch(psModule.reject)
@@ -109,7 +114,11 @@ module.exports = function purescriptLoader(source, map) {
109 cache.deferred.push(psModule) 114 cache.deferred.push(psModule)
110 115
111 if (!cache.compilationStarted) { 116 if (!cache.compilationStarted) {
112 return compile(psModule) 117 return Psc.compile(psModule)
118 .then(() => PsModuleMap.makeMap(options.src).then(map => {
119 debug('rebuilt module map');
120 cache.psModuleMap = map;
121 }))
113 .then(() => Promise.map(cache.deferred, psModule => { 122 .then(() => Promise.map(cache.deferred, psModule => {
114 if (typeof cache.ideServer === 'object') cache.ideServer.kill() 123 if (typeof cache.ideServer === 'object') cache.ideServer.kill()
115 return toJavaScript(psModule).then(psModule.load) 124 return toJavaScript(psModule).then(psModule.load)
@@ -121,6 +130,26 @@ module.exports = function purescriptLoader(source, map) {
121 } 130 }
122} 131}
123 132
133function updatePsModuleMap(psModule) {
134 const options = psModule.options
135 const cache = psModule.cache
136 const filePurs = psModule.srcPath
137 if (!cache.psModuleMap) {
138 debug('module mapping does not exist');
139 return PsModuleMap.makeMap(options.src).then(map => {
140 cache.psModuleMap = map;
141 return cache.psModuleMap;
142 });
143 }
144 else {
145 return PsModuleMap.makeMapEntry(filePurs).then(result => {
146 const map = Object.assign(cache.psModuleMap, result)
147 cache.psModuleMap = map;
148 return cache.psModuleMap;
149 });
150 }
151}
152
124// The actual loader is executed *after* purescript compilation. 153// The actual loader is executed *after* purescript compilation.
125function toJavaScript(psModule) { 154function toJavaScript(psModule) {
126 const options = psModule.options 155 const options = psModule.options
@@ -132,7 +161,7 @@ function toJavaScript(psModule) {
132 161
133 return Promise.props({ 162 return Promise.props({
134 js: fs.readFileAsync(jsPath, 'utf8'), 163 js: fs.readFileAsync(jsPath, 'utf8'),
135 psModuleMap: psModuleMap(options, cache) 164 psModuleMap: updatePsModuleMap(psModule)
136 }).then(result => { 165 }).then(result => {
137 let js = '' 166 let js = ''
138 167
@@ -156,346 +185,3 @@ function toJavaScript(psModule) {
156 return js 185 return js
157 }) 186 })
158} 187}
159
160function compile(psModule) {
161 const options = psModule.options
162 const cache = psModule.cache
163 const stderr = []
164
165 if (cache.compilationStarted) return Promise.resolve(psModule)
166
167 cache.compilationStarted = true
168
169 const args = dargs(Object.assign({
170 _: options.src,
171 output: options.output,
172 }, options.pscArgs))
173
174 debug('spawning compiler %s %o', options.psc, args)
175
176 return (new Promise((resolve, reject) => {
177 console.log('\nCompiling PureScript...')
178
179 const compilation = spawn(options.psc, args)
180
181 compilation.stdout.on('data', data => stderr.push(data.toString()))
182 compilation.stderr.on('data', data => stderr.push(data.toString()))
183
184 compilation.on('close', code => {
185 console.log('Finished compiling PureScript.')
186 cache.compilationFinished = true
187 if (code !== 0) {
188 cache.errors = stderr.join('')
189 reject(true)
190 } else {
191 cache.warnings = stderr.join('')
192 resolve(psModule)
193 }
194 })
195 }))
196 .then(compilerOutput => {
197 if (options.bundle) {
198 return bundle(options, cache).then(() => psModule)
199 }
200 return psModule
201 })
202}
203
204function rebuild(psModule) {
205 const options = psModule.options
206 const cache = psModule.cache
207
208 debug('attempting rebuild with psc-ide-client %s', psModule.srcPath)
209
210 const request = (body) => new Promise((resolve, reject) => {
211 const args = dargs(options.pscIdeArgs)
212 const ideClient = spawn('psc-ide-client', args)
213
214 var stdout = ''
215 var stderr = ''
216
217 ideClient.stdout.on('data', data => {
218 stdout = stdout + data.toString()
219 })
220
221 ideClient.stderr.on('data', data => {
222 stderr = stderr + data.toString()
223 })
224
225 ideClient.on('close', code => {
226 if (code !== 0) {
227 const error = stderr === '' ? 'Failed to spawn psc-ide-client' : stderr
228 return reject(new Error(error))
229 }
230
231 let res = null
232
233 try {
234 res = JSON.parse(stdout.toString())
235 debug(res)
236 } catch (err) {
237 return reject(err)
238 }
239
240 if (res && !Array.isArray(res.result)) {
241 return res.resultType === 'success'
242 ? resolve(psModule)
243 : reject('psc-ide rebuild failed')
244 }
245
246 Promise.map(res.result, (item, i) => {
247 debug(item)
248 return formatIdeResult(item, options, i, res.result.length)
249 })
250 .then(compileMessages => {
251 if (res.resultType === 'error') {
252 if (res.result.some(item => item.errorCode === 'UnknownModule')) {
253 console.log('Unknown module, attempting full recompile')
254 return compile(psModule)
255 .then(() => request({ command: 'load' }))
256 .then(resolve)
257 .catch(() => reject('psc-ide rebuild failed'))
258 }
259 cache.errors = compileMessages.join('\n')
260 reject('psc-ide rebuild failed')
261 } else {
262 cache.warnings = compileMessages.join('\n')
263 resolve(psModule)
264 }
265 })
266 })
267
268 ideClient.stdin.write(JSON.stringify(body))
269 ideClient.stdin.write('\n')
270 })
271
272 return request({
273 command: 'rebuild',
274 params: {
275 file: psModule.srcPath,
276 }
277 })
278}
279
280function formatIdeResult(result, options, index, length) {
281 const srcPath = path.relative(options.context, result.filename)
282 const pos = result.position
283 const fileAndPos = `${srcPath}:${pos.startLine}:${pos.startColumn}`
284 let numAndErr = `[${index+1}/${length} ${result.errorCode}]`
285 numAndErr = options.pscIdeColors ? colors.yellow(numAndErr) : numAndErr
286
287 return fs.readFileAsync(result.filename, 'utf8').then(source => {
288 const lines = source.split('\n').slice(pos.startLine - 1, pos.endLine)
289 const endsOnNewline = pos.endColumn === 1 && pos.startLine !== pos.endLine
290 const up = options.pscIdeColors ? colors.red('^') : '^'
291 const down = options.pscIdeColors ? colors.red('v') : 'v'
292 let trimmed = lines.slice(0)
293
294 if (endsOnNewline) {
295 lines.splice(lines.length - 1, 1)
296 pos.endLine = pos.endLine - 1
297 pos.endColumn = lines[lines.length - 1].length || 1
298 }
299
300 // strip newlines at the end
301 if (endsOnNewline) {
302 trimmed = lines.reverse().reduce((trimmed, line, i) => {
303 if (i === 0 && line === '') trimmed.trimming = true
304 if (!trimmed.trimming) trimmed.push(line)
305 if (trimmed.trimming && line !== '') {
306 trimmed.trimming = false
307 trimmed.push(line)
308 }
309 return trimmed
310 }, []).reverse()
311 pos.endLine = pos.endLine - (lines.length - trimmed.length)
312 pos.endColumn = trimmed[trimmed.length - 1].length || 1
313 }
314
315 const spaces = ' '.repeat(String(pos.endLine).length)
316 let snippet = trimmed.map((line, i) => {
317 return ` ${pos.startLine + i} ${line}`
318 }).join('\n')
319
320 if (trimmed.length === 1) {
321 snippet += `\n ${spaces} ${' '.repeat(pos.startColumn - 1)}${up.repeat(pos.endColumn - pos.startColumn + 1)}`
322 } else {
323 snippet = ` ${spaces} ${' '.repeat(pos.startColumn - 1)}${down}\n${snippet}`
324 snippet += `\n ${spaces} ${' '.repeat(pos.endColumn - 1)}${up}`
325 }
326
327 return Promise.resolve(
328 `\n${numAndErr} ${fileAndPos}\n\n${snippet}\n\n${result.message}`
329 )
330 })
331}
332
333function bundle(options, cache) {
334 if (cache.bundle) return Promise.resolve(cache.bundle)
335
336 const stdout = []
337 const stderr = cache.bundle = []
338
339 const args = dargs(Object.assign({
340 _: [path.join(options.output, '*', '*.js')],
341 output: options.bundleOutput,
342 namespace: options.bundleNamespace,
343 }, options.pscBundleArgs))
344
345 cache.bundleModules.forEach(name => args.push('--module', name))
346
347 debug('spawning bundler %s %o', options.pscBundle, args.join(' '))
348
349 return (new Promise((resolve, reject) => {
350 console.log('Bundling PureScript...')
351
352 const compilation = spawn(options.pscBundle, args)
353
354 compilation.stdout.on('data', data => stdout.push(data.toString()))
355 compilation.stderr.on('data', data => stderr.push(data.toString()))
356 compilation.on('close', code => {
357 if (code !== 0) {
358 cache.errors = (cache.errors || '') + stderr.join('')
359 return reject(true)
360 }
361 cache.bundle = stderr
362 resolve(fs.appendFileAsync('output/bundle.js', `module.exports = ${options.bundleNamespace}`))
363 })
364 }))
365}
366
367// map of PS module names to their source path
368function psModuleMap(options, cache) {
369 if (cache.psModuleMap) return Promise.resolve(cache.psModuleMap)
370
371 const globs = [].concat(options.src);
372
373 function pursToJs(file){
374 const dirname = path.dirname(file)
375 const basename = path.basename(file, '.purs')
376 const fileJS = path.join(dirname, `${basename}.js`)
377 return fileJS
378 }
379
380 return globby(globs).then(paths => {
381 return Promise
382 .props(paths.reduce((map, file) => {
383 const fileJS = pursToJs(file)
384 map[file] = fs.readFileAsync(file, 'utf8')
385 map[fileJS] = fs.readFileAsync(fileJS, 'utf8').catch(() => undefined)
386 return map
387 }, {}))
388 .then(fileMap => {
389 cache.psModuleMap = Object.keys(fileMap).reduce((map, file) => {
390 const ext = path.extname(file)
391 const isPurs = ext.match(/purs$/i)
392 if (isPurs) {
393 const fileJs = pursToJs(file)
394 const source = fileMap[file]
395 const ffi = fileMap[fileJs]
396 const moduleName = match(srcModuleRegex, source)
397 map[moduleName] = map[moduleName] || {}
398 map[moduleName].src = path.resolve(file)
399 if (ffi) {
400 map[moduleName].ffi = path.resolve(fileJs)
401 }
402 }
403 return map
404 }, {})
405 return cache.psModuleMap
406 })
407 })
408}
409
410function connectIdeServer(psModule) {
411 const options = psModule.options
412 const cache = psModule.cache
413
414 if (cache.ideServer) return Promise.resolve(psModule)
415
416 cache.ideServer = true
417
418 const connect = () => new Promise((resolve, reject) => {
419 const args = dargs(options.pscIdeArgs)
420
421 debug('attempting to connect to psc-ide-server', args)
422
423 const ideClient = spawn('psc-ide-client', args)
424
425 ideClient.stderr.on('data', data => {
426 debug(data.toString())
427 cache.ideServer = false
428 reject(true)
429 })
430 ideClient.stdout.once('data', data => {
431 debug(data.toString())
432 if (data.toString()[0] === '{') {
433 const res = JSON.parse(data.toString())
434 if (res.resultType === 'success') {
435 cache.ideServer = ideServer
436 resolve(psModule)
437 } else {
438 cache.ideServer = ideServer
439 reject(true)
440 }
441 } else {
442 cache.ideServer = false
443 reject(true)
444 }
445 })
446 ideClient.stdin.resume()
447 ideClient.stdin.write(JSON.stringify({ command: 'load' }))
448 ideClient.stdin.write('\n')
449 })
450
451 const args = dargs(Object.assign({
452 outputDirectory: options.output,
453 }, options.pscIdeArgs))
454
455 debug('attempting to start psc-ide-server', args)
456
457 const ideServer = cache.ideServer = spawn('psc-ide-server', [])
458 ideServer.stderr.on('data', data => {
459 debug(data.toString())
460 })
461
462 return retryPromise((retry, number) => {
463 return connect().catch(error => {
464 if (!cache.ideServer && number === 9) {
465 debug(error)
466
467 console.log(
468 'failed to connect to or start psc-ide-server, ' +
469 'full compilation will occur on rebuild'
470 )
471
472 return Promise.resolve(psModule)
473 }
474
475 return retry(error)
476 })
477 }, {
478 retries: 9,
479 factor: 1,
480 minTimeout: 333,
481 maxTimeout: 333,
482 })
483}
484
485function match(regex, str) {
486 const matches = str.match(regex)
487 return matches && matches[1]
488}
489
490function dargs(obj) {
491 return Object.keys(obj).reduce((args, key) => {
492 const arg = '--' + key.replace(/[A-Z]/g, '-$&').toLowerCase();
493 const val = obj[key]
494
495 if (key === '_') val.forEach(v => args.push(v))
496 else if (Array.isArray(val)) val.forEach(v => args.push(arg, v))
497 else args.push(arg, obj[key])
498
499 return args.filter(arg => (typeof arg !== 'boolean'))
500 }, [])
501}