aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authoreric thul <thul.eric@gmail.com>2016-06-12 15:17:44 -0400
committereric thul <thul.eric@gmail.com>2016-06-12 15:17:44 -0400
commit531c751fe5593750a377db38bcfaf9a5383ac661 (patch)
tree935b5f17cc29bee58e27b474fe604a3257b7fc63
parent7243be70a2163be2230a5f2739768137305a24ef (diff)
downloadpurs-loader-531c751fe5593750a377db38bcfaf9a5383ac661.tar.gz
purs-loader-531c751fe5593750a377db38bcfaf9a5383ac661.tar.zst
purs-loader-531c751fe5593750a377db38bcfaf9a5383ac661.zip
Reduce building of PureScript module map
Resolves #59 and resolves #60
-rw-r--r--.gitignore2
-rw-r--r--package.json8
-rw-r--r--src/PsModuleMap.js66
-rw-r--r--src/Psc.js92
-rw-r--r--src/PscIde.js231
-rw-r--r--src/dargs.js16
-rw-r--r--src/index.js394
7 files changed, 449 insertions, 360 deletions
diff --git a/.gitignore b/.gitignore
index 7d50432..4b26e66 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,3 @@
1**DS_Store* 1**DS_Store*
2node_modules/ 2node_modules/
3index.js 3lib/
diff --git a/package.json b/package.json
index 5fc5a7f..6c276ce 100644
--- a/package.json
+++ b/package.json
@@ -2,15 +2,15 @@
2 "name": "purs-loader", 2 "name": "purs-loader",
3 "version": "2.0.0-rc.0", 3 "version": "2.0.0-rc.0",
4 "description": "A webpack loader for PureScript.", 4 "description": "A webpack loader for PureScript.",
5 "main": "index.js", 5 "main": "lib/index.js",
6 "files": [ 6 "files": [
7 "LICENSE", 7 "LICENSE",
8 "README.md", 8 "README.md",
9 "index.js", 9 "src",
10 "src/index.js" 10 "lib"
11 ], 11 ],
12 "scripts": { 12 "scripts": {
13 "build": "babel src/index.js -o index.js", 13 "build": "babel src --out-dir lib",
14 "prepublish": "npm run build", 14 "prepublish": "npm run build",
15 "test": "echo \"Error: no test specified\" && exit 1" 15 "test": "echo \"Error: no test specified\" && exit 1"
16 }, 16 },
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..d99b639
--- /dev/null
+++ b/src/PscIde.js
@@ -0,0 +1,231 @@
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 const srcPath = path.relative(options.context, result.filename)
182 const pos = result.position
183 const fileAndPos = `${srcPath}:${pos.startLine}:${pos.startColumn}`
184 let numAndErr = `[${index+1}/${length} ${result.errorCode}]`
185 numAndErr = options.pscIdeColors ? colors.yellow(numAndErr) : numAndErr
186
187 return fs.readFileAsync(result.filename, 'utf8').then(source => {
188 const lines = source.split('\n').slice(pos.startLine - 1, pos.endLine)
189 const endsOnNewline = pos.endColumn === 1 && pos.startLine !== pos.endLine
190 const up = options.pscIdeColors ? colors.red('^') : '^'
191 const down = options.pscIdeColors ? colors.red('v') : 'v'
192 let trimmed = lines.slice(0)
193
194 if (endsOnNewline) {
195 lines.splice(lines.length - 1, 1)
196 pos.endLine = pos.endLine - 1
197 pos.endColumn = lines[lines.length - 1].length || 1
198 }
199
200 // strip newlines at the end
201 if (endsOnNewline) {
202 trimmed = lines.reverse().reduce((trimmed, line, i) => {
203 if (i === 0 && line === '') trimmed.trimming = true
204 if (!trimmed.trimming) trimmed.push(line)
205 if (trimmed.trimming && line !== '') {
206 trimmed.trimming = false
207 trimmed.push(line)
208 }
209 return trimmed
210 }, []).reverse()
211 pos.endLine = pos.endLine - (lines.length - trimmed.length)
212 pos.endColumn = trimmed[trimmed.length - 1].length || 1
213 }
214
215 const spaces = ' '.repeat(String(pos.endLine).length)
216 let snippet = trimmed.map((line, i) => {
217 return ` ${pos.startLine + i} ${line}`
218 }).join('\n')
219
220 if (trimmed.length === 1) {
221 snippet += `\n ${spaces} ${' '.repeat(pos.startColumn - 1)}${up.repeat(pos.endColumn - pos.startColumn + 1)}`
222 } else {
223 snippet = ` ${spaces} ${' '.repeat(pos.startColumn - 1)}${down}\n${snippet}`
224 snippet += `\n ${spaces} ${' '.repeat(pos.endColumn - 1)}${up}`
225 }
226
227 return Promise.resolve(
228 `\n${numAndErr} ${fileAndPos}\n\n${snippet}\n\n${result.message}`
229 )
230 })
231}
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..fe1455b 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
@@ -74,7 +77,7 @@ module.exports = function purescriptLoader(source, map) {
74 }) 77 })
75 } 78 }
76 79
77 const psModuleName = match(srcModuleRegex, source) 80 const psModuleName = PsModuleMap.match(source)
78 const psModule = { 81 const psModule = {
79 name: psModuleName, 82 name: psModuleName,
80 load: js => callback(null, js), 83 load: js => callback(null, js),
@@ -93,8 +96,8 @@ module.exports = function purescriptLoader(source, map) {
93 } 96 }
94 97
95 if (cache.rebuild) { 98 if (cache.rebuild) {
96 return connectIdeServer(psModule) 99 return PscIde.connect(psModule)
97 .then(rebuild) 100 .then(PscIde.rebuild)
98 .then(toJavaScript) 101 .then(toJavaScript)
99 .then(psModule.load) 102 .then(psModule.load)
100 .catch(psModule.reject) 103 .catch(psModule.reject)
@@ -109,7 +112,11 @@ module.exports = function purescriptLoader(source, map) {
109 cache.deferred.push(psModule) 112 cache.deferred.push(psModule)
110 113
111 if (!cache.compilationStarted) { 114 if (!cache.compilationStarted) {
112 return compile(psModule) 115 return Psc.compile(psModule)
116 .then(() => PsModuleMap.makeMap(options.src).then(map => {
117 debug('rebuilt module map');
118 cache.psModuleMap = map;
119 }))
113 .then(() => Promise.map(cache.deferred, psModule => { 120 .then(() => Promise.map(cache.deferred, psModule => {
114 if (typeof cache.ideServer === 'object') cache.ideServer.kill() 121 if (typeof cache.ideServer === 'object') cache.ideServer.kill()
115 return toJavaScript(psModule).then(psModule.load) 122 return toJavaScript(psModule).then(psModule.load)
@@ -121,6 +128,26 @@ module.exports = function purescriptLoader(source, map) {
121 } 128 }
122} 129}
123 130
131function updatePsModuleMap(psModule) {
132 const options = psModule.options
133 const cache = psModule.cache
134 const filePurs = psModule.srcPath
135 if (!cache.psModuleMap) {
136 debug('module mapping does not exist');
137 return PsModuleMap.makeMap(options.src).then(map => {
138 cache.psModuleMap = map;
139 return cache.psModuleMap;
140 });
141 }
142 else {
143 return PsModuleMap.makeMapEntry(filePurs).then(result => {
144 const map = Object.assign(cache.psModuleMap, result)
145 cache.psModuleMap = map;
146 return cache.psModuleMap;
147 });
148 }
149}
150
124// The actual loader is executed *after* purescript compilation. 151// The actual loader is executed *after* purescript compilation.
125function toJavaScript(psModule) { 152function toJavaScript(psModule) {
126 const options = psModule.options 153 const options = psModule.options
@@ -132,7 +159,7 @@ function toJavaScript(psModule) {
132 159
133 return Promise.props({ 160 return Promise.props({
134 js: fs.readFileAsync(jsPath, 'utf8'), 161 js: fs.readFileAsync(jsPath, 'utf8'),
135 psModuleMap: psModuleMap(options, cache) 162 psModuleMap: updatePsModuleMap(psModule)
136 }).then(result => { 163 }).then(result => {
137 let js = '' 164 let js = ''
138 165
@@ -156,346 +183,3 @@ function toJavaScript(psModule) {
156 return js 183 return js
157 }) 184 })
158} 185}
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}