diff options
author | eric thul <thul.eric@gmail.com> | 2016-06-12 15:17:44 -0400 |
---|---|---|
committer | eric thul <thul.eric@gmail.com> | 2016-06-12 15:17:44 -0400 |
commit | 531c751fe5593750a377db38bcfaf9a5383ac661 (patch) | |
tree | 935b5f17cc29bee58e27b474fe604a3257b7fc63 /src/index.js | |
parent | 7243be70a2163be2230a5f2739768137305a24ef (diff) | |
download | purs-loader-531c751fe5593750a377db38bcfaf9a5383ac661.tar.gz purs-loader-531c751fe5593750a377db38bcfaf9a5383ac661.tar.zst purs-loader-531c751fe5593750a377db38bcfaf9a5383ac661.zip |
Reduce building of PureScript module map
Resolves #59 and resolves #60
Diffstat (limited to 'src/index.js')
-rw-r--r-- | src/index.js | 394 |
1 files changed, 39 insertions, 355 deletions
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 | ||
3 | const colors = require('chalk') | ||
4 | const debug = require('debug')('purs-loader') | 3 | const debug = require('debug')('purs-loader') |
5 | const loaderUtils = require('loader-utils') | 4 | const loaderUtils = require('loader-utils') |
6 | const globby = require('globby') | ||
7 | const Promise = require('bluebird') | 5 | const Promise = require('bluebird') |
8 | const fs = Promise.promisifyAll(require('fs')) | 6 | const fs = Promise.promisifyAll(require('fs')) |
9 | const spawn = require('cross-spawn') | ||
10 | const path = require('path') | 7 | const path = require('path') |
11 | const retryPromise = require('promise-retry') | ||
12 | const jsStringEscape = require('js-string-escape') | 8 | const jsStringEscape = require('js-string-escape') |
9 | const PsModuleMap = require('./PsModuleMap'); | ||
10 | const Psc = require('./Psc'); | ||
11 | const PscIde = require('./PscIde'); | ||
12 | const dargs = require('./dargs'); | ||
13 | 13 | ||
14 | const srcModuleRegex = /(?:^|\n)module\s+([\w\.]+)/i | ||
15 | const requireRegex = /require\(['"]\.\.\/([\w\.]+)['"]\)/g | 14 | const requireRegex = /require\(['"]\.\.\/([\w\.]+)['"]\)/g |
16 | 15 | ||
17 | module.exports = function purescriptLoader(source, map) { | 16 | module.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 | ||
131 | function 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. |
125 | function toJavaScript(psModule) { | 152 | function 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 | |||
160 | function 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 | |||
204 | function 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 | |||
280 | function 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 | |||
333 | function 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 | ||
368 | function 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 | |||
410 | function 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 | |||
485 | function match(regex, str) { | ||
486 | const matches = str.match(regex) | ||
487 | return matches && matches[1] | ||
488 | } | ||
489 | |||
490 | function 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 | } | ||