X-Git-Url: https://git.immae.eu/?a=blobdiff_plain;f=src%2Findex.js;h=c73fdd5e88cc2c76f925abd85353d28b80f61d2a;hb=2cca7e91062917d3239f179770133175350af7cc;hp=0a25ccdbced715eaa79c03407969bddb50de84cf;hpb=7de41f10b4ff0f0d6b45d59bee0f166c3cfe3f9f;p=github%2Ffretlink%2Fpurs-loader.git diff --git a/src/index.js b/src/index.js index 0a25ccd..c73fdd5 100644 --- a/src/index.js +++ b/src/index.js @@ -1,16 +1,16 @@ 'use strict' -const colors = require('chalk') const debug = require('debug')('purs-loader') const loaderUtils = require('loader-utils') -const globby = require('globby') const Promise = require('bluebird') const fs = Promise.promisifyAll(require('fs')) -const spawn = require('child_process').spawn const path = require('path') -const retryPromise = require('promise-retry') +const jsStringEscape = require('js-string-escape') +const PsModuleMap = require('./PsModuleMap'); +const Psc = require('./Psc'); +const PscIde = require('./PscIde'); +const dargs = require('./dargs'); -const psModuleRegex = /(?:^|\n)module\s+([\w\.]+)/i const requireRegex = /require\(['"]\.\.\/([\w\.]+)['"]\)/g module.exports = function purescriptLoader(source, map) { @@ -25,6 +25,7 @@ module.exports = function purescriptLoader(source, map) { pscArgs: {}, pscBundle: 'psc-bundle', pscBundleArgs: {}, + pscIde: false, pscIdeColors: webpackOptions.psc === 'psa' || query.psc === 'psa', pscIdeArgs: {}, bundleOutput: 'output/bundle.js', @@ -35,11 +36,7 @@ module.exports = function purescriptLoader(source, map) { src: [ path.join('src', '**', '*.purs'), path.join('bower_components', 'purescript-*', 'src', '**', '*.purs') - ], - ffi: [ - path.join('src', '**', '*.js'), - path.join('bower_components', 'purescript-*', 'src', '**', '*.js') - ], + ] }, webpackOptions, query) this.cacheable && this.cacheable() @@ -47,7 +44,7 @@ module.exports = function purescriptLoader(source, map) { let cache = config.purescriptLoaderCache = config.purescriptLoaderCache || { rebuild: false, deferred: [], - bundleModules: [], + bundleModules: [] } if (!config.purescriptLoaderInstalled) { @@ -55,28 +52,34 @@ module.exports = function purescriptLoader(source, map) { // invalidate loader cache when bundle is marked as invalid (in watch mode) this._compiler.plugin('invalid', () => { + debug('invalidating loader cache'); + cache = config.purescriptLoaderCache = { - rebuild: true, + rebuild: options.pscIde, deferred: [], - ideServer: cache.ideServer + bundleModules: [], + ideServer: cache.ideServer, + psModuleMap: cache.psModuleMap } }) // add psc warnings to webpack compilation warnings this._compiler.plugin('after-compile', (compilation, callback) => { - if (options.warnings && cache.warnings && cache.warnings.length) { - compilation.warnings.unshift(`PureScript compilation:\n${cache.warnings.join('')}`) + if (options.warnings && cache.warnings) { + compilation.warnings.unshift(`PureScript compilation:\n${cache.warnings}`) + cache.warnings = null; } - if (cache.errors && cache.errors.length) { - compilation.errors.unshift(`PureScript compilation:\n${cache.errors.join('\n')}`) + if (cache.errors) { + compilation.errors.unshift(`PureScript compilation:\n${cache.errors}`) + cache.errors = null; } callback() }) } - const psModuleName = match(psModuleRegex, source) + const psModuleName = PsModuleMap.match(source) const psModule = { name: psModuleName, load: js => callback(null, js), @@ -88,19 +91,21 @@ module.exports = function purescriptLoader(source, map) { cache: cache, } + debug('loader called', psModule.name) + if (options.bundle) { cache.bundleModules.push(psModule.name) } if (cache.rebuild) { - return connectIdeServer(psModule) - .then(rebuild) + return PscIde.connect(psModule) + .then(PscIde.rebuild) .then(toJavaScript) .then(psModule.load) .catch(psModule.reject) } - if (cache.compilation && cache.compilation.length) { + if (cache.compilationFinished) { return toJavaScript(psModule).then(psModule.load).catch(psModule.reject) } @@ -108,8 +113,12 @@ module.exports = function purescriptLoader(source, map) { // references to compiled output are valid. cache.deferred.push(psModule) - if (!cache.compilation) { - return compile(psModule) + if (!cache.compilationStarted) { + return Psc.compile(psModule) + .then(() => PsModuleMap.makeMap(options.src).then(map => { + debug('rebuilt module map'); + cache.psModuleMap = map; + })) .then(() => Promise.map(cache.deferred, psModule => { if (typeof cache.ideServer === 'object') cache.ideServer.kill() return toJavaScript(psModule).then(psModule.load) @@ -121,6 +130,26 @@ module.exports = function purescriptLoader(source, map) { } } +function updatePsModuleMap(psModule) { + const options = psModule.options + const cache = psModule.cache + const filePurs = psModule.srcPath + if (!cache.psModuleMap) { + debug('module mapping does not exist'); + return PsModuleMap.makeMap(options.src).then(map => { + cache.psModuleMap = map; + return cache.psModuleMap; + }); + } + else { + return PsModuleMap.makeMapEntry(filePurs).then(result => { + const map = Object.assign(cache.psModuleMap, result) + cache.psModuleMap = map; + return cache.psModuleMap; + }); + } +} + // The actual loader is executed *after* purescript compilation. function toJavaScript(psModule) { const options = psModule.options @@ -128,338 +157,31 @@ function toJavaScript(psModule) { const bundlePath = path.resolve(options.bundleOutput) const jsPath = cache.bundle ? bundlePath : psModule.jsPath - debug('loading JavaScript for', psModule.srcPath) + debug('loading JavaScript for', psModule.name) return Promise.props({ js: fs.readFileAsync(jsPath, 'utf8'), - psModuleMap: psModuleMap(options.src, cache) + psModuleMap: updatePsModuleMap(psModule) }).then(result => { let js = '' if (options.bundle) { // if bundling, return a reference to the bundle js = 'module.exports = require("' - + path.relative(psModule.srcDir, options.bundleOutput) + + jsStringEscape(path.relative(psModule.srcDir, options.bundleOutput)) + '")["' + psModule.name + '"]' } else { // replace require paths to output files generated by psc with paths // to purescript sources, which are then also run through this loader. - const foreignRequire = 'require("' + path.resolve( - path.join(psModule.options.output, psModule.name, 'foreign.js') - ) + '")' - js = result.js .replace(requireRegex, (m, p1) => { - return 'require("' + result.psModuleMap[p1] + '")' + return 'require("' + jsStringEscape(result.psModuleMap[p1].src) + '")' + }) + .replace(/require\(['"]\.\/foreign['"]\)/g, (m, p1) => { + return 'require("' + jsStringEscape(result.psModuleMap[psModule.name].ffi) + '")' }) - .replace(/require\(['"]\.\/foreign['"]\)/g, foreignRequire) } return js }) } - -function compile(psModule) { - const options = psModule.options - const cache = psModule.cache - const stderr = [] - - if (cache.compilation) return Promise.resolve(cache.compilation) - - cache.compilation = [] - cache.warnings = [] - cache.errors = [] - - - const args = dargs(Object.assign({ - _: options.src, - ffi: options.ffi, - output: options.output, - }, options.pscArgs)) - - debug('spawning compiler %s %o', options.psc, args) - - return (new Promise((resolve, reject) => { - console.log('\nCompiling PureScript...') - - const compilation = spawn(options.psc, args) - - compilation.stderr.on('data', data => stderr.push(data.toString())) - - compilation.on('close', code => { - console.log('Finished compiling PureScript.') - if (code !== 0) { - cache.compilation = cache.errors = stderr - reject(true) - } else { - cache.compilation = cache.warnings = stderr - resolve(psModule) - } - }) - })) - .then(compilerOutput => { - if (options.bundle) { - return bundle(options, cache).then(() => psModule) - } - return psModule - }) -} - -function rebuild(psModule) { - const options = psModule.options - const cache = psModule.cache - - debug('attempting rebuild with psc-ide-client %s', psModule.srcPath) - - const request = (body) => new Promise((resolve, reject) => { - const args = dargs(options.pscIdeArgs) - const ideClient = spawn('psc-ide-client', args) - - ideClient.stdout.once('data', data => { - const res = JSON.parse(data.toString()) - debug(res) - - if (!Array.isArray(res.result)) { - return res.resultType === 'success' - ? resolve(psModule) - : reject(res.result) - } - - Promise.map(res.result, (item, i) => { - debug(item) - return formatIdeResult(item, options, i, res.result.length) - }) - .then(compileMessages => { - if (res.resultType === 'error') { - cache.errors = compileMessages - reject(res.result) - } else { - cache.warnings = compileMessages - resolve(psModule) - } - }) - }) - - ideClient.stderr.once('data', data => reject(data.toString())) - - ideClient.stdin.write(JSON.stringify(body)) - ideClient.stdin.write('\n') - }) - - return request({ - command: 'rebuild', - params: { - file: psModule.srcPath, - } - }).catch(res => { - if (res.resultType === 'error') { - if (res.result.some(item => item.errorCode === 'UnknownModule')) { - console.log('Unknown module, attempting full recompile') - return compile(psModule).then(() => request({ command: 'load' })) - } - } - return Promise.resolve(psModule) - }) -} - -function formatIdeResult(result, options, index, length) { - const srcPath = path.relative(options.context, result.filename) - const pos = result.position - const fileAndPos = `${srcPath}:${pos.startLine}:${pos.startColumn}` - let numAndErr = `[${index+1}/${length} ${result.errorCode}]` - numAndErr = options.pscIdeColors ? colors.yellow(numAndErr) : numAndErr - - return fs.readFileAsync(result.filename, 'utf8').then(source => { - const lines = source.split('\n').slice(pos.startLine - 1, pos.endLine) - const endsOnNewline = pos.endColumn === 1 && pos.startLine !== pos.endLine - const up = options.pscIdeColors ? colors.red('^') : '^' - const down = options.pscIdeColors ? colors.red('v') : 'v' - let trimmed = lines.slice(0) - - if (endsOnNewline) { - lines.splice(lines.length - 1, 1) - pos.endLine = pos.endLine - 1 - pos.endColumn = lines[lines.length - 1].length || 1 - } - - // strip newlines at the end - if (endsOnNewline) { - trimmed = lines.reverse().reduce((trimmed, line, i) => { - if (i === 0 && line === '') trimmed.trimming = true - if (!trimmed.trimming) trimmed.push(line) - if (trimmed.trimming && line !== '') { - trimmed.trimming = false - trimmed.push(line) - } - return trimmed - }, []).reverse() - pos.endLine = pos.endLine - (lines.length - trimmed.length) - pos.endColumn = trimmed[trimmed.length - 1].length || 1 - } - - const spaces = ' '.repeat(String(pos.endLine).length) - let snippet = trimmed.map((line, i) => { - return ` ${pos.startLine + i} ${line}` - }).join('\n') - - if (trimmed.length === 1) { - snippet += `\n ${spaces} ${' '.repeat(pos.startColumn - 1)}${up.repeat(pos.endColumn - pos.startColumn + 1)}` - } else { - snippet = ` ${spaces} ${' '.repeat(pos.startColumn - 1)}${down}\n${snippet}` - snippet += `\n ${spaces} ${' '.repeat(pos.endColumn - 1)}${up}` - } - - return Promise.resolve( - `\n${numAndErr} ${fileAndPos}\n\n${snippet}\n\n${result.message}` - ) - }) -} - -function bundle(options, cache) { - if (cache.bundle) return Promise.resolve(cache.bundle) - - const stdout = [] - const stderr = cache.bundle = [] - - const args = dargs(Object.assign({ - _: [path.join(options.output, '*', '*.js')], - output: options.bundleOutput, - namespace: options.bundleNamespace, - }, options.pscBundleArgs)) - - cache.bundleModules.forEach(name => args.push('--module', name)) - - debug('spawning bundler %s %o', options.pscBundle, args.join(' ')) - - return (new Promise((resolve, reject) => { - console.log('Bundling PureScript...') - - const compilation = spawn(options.pscBundle, args) - - compilation.stdout.on('data', data => stdout.push(data.toString())) - compilation.stderr.on('data', data => stderr.push(data.toString())) - compilation.on('close', code => { - if (code !== 0) { - cache.errors.concat(stderr) - return reject(true) - } - cache.bundle = stderr - resolve(fs.appendFileAsync('output/bundle.js', `module.exports = ${options.bundleNamespace}`)) - }) - })) -} - -// map of PS module names to their source path -function psModuleMap(globs, cache) { - if (cache.psModuleMap) return Promise.resolve(cache.psModuleMap) - - return globby(globs).then(paths => { - return Promise - .props(paths.reduce((map, file) => { - map[file] = fs.readFileAsync(file, 'utf8') - return map - }, {})) - .then(srcMap => { - cache.psModuleMap = Object.keys(srcMap).reduce((map, file) => { - const source = srcMap[file] - const psModuleName = match(psModuleRegex, source) - map[psModuleName] = path.resolve(file) - return map - }, {}) - return cache.psModuleMap - }) - }) -} - -function connectIdeServer(psModule) { - const options = psModule.options - const cache = psModule.cache - - if (cache.ideServer) return Promise.resolve(psModule) - - cache.ideServer = true - - const connect = () => new Promise((resolve, reject) => { - const args = dargs(options.pscIdeArgs) - - debug('attempting to connect to psc-ide-server', args) - - const ideClient = spawn('psc-ide-client', args) - - ideClient.stderr.on('data', data => { - debug(data.toString()) - cache.ideServer = false - reject(true) - }) - ideClient.stdout.once('data', data => { - debug(data.toString()) - if (data.toString()[0] === '{') { - const res = JSON.parse(data.toString()) - if (res.resultType === 'success') { - cache.ideServer = ideServer - resolve(psModule) - } else { - cache.ideServer = ideServer - reject(true) - } - } else { - cache.ideServer = false - reject(true) - } - }) - ideClient.stdin.resume() - ideClient.stdin.write(JSON.stringify({ command: 'load' })) - ideClient.stdin.write('\n') - }) - - const args = dargs(Object.assign({ - outputDirectory: options.output, - }, options.pscIdeArgs)) - - debug('attempting to start psc-ide-server', args) - - const ideServer = cache.ideServer = spawn('psc-ide-server', []) - ideServer.stderr.on('data', data => { - debug(data.toString()) - }) - - return retryPromise((retry, number) => { - return connect().catch(error => { - if (!cache.ideServer && number === 9) { - debug(error) - - console.log( - 'failed to connect to or start psc-ide-server, ' + - 'full compilation will occur on rebuild' - ) - - return Promise.resolve(psModule) - } - - return retry(error) - }) - }, { - retries: 9, - factor: 1, - minTimeout: 333, - maxTimeout: 333, - }) -} - -function match(regex, str) { - const matches = str.match(regex) - return matches && matches[1] -} - -function dargs(obj) { - return Object.keys(obj).reduce((args, key) => { - const arg = '--' + key.replace(/[A-Z]/g, '-$&').toLowerCase(); - const val = obj[key] - - if (key === '_') val.forEach(v => args.push(v)) - else if (Array.isArray(val)) val.forEach(v => args.push(arg, v)) - else args.push(arg, obj[key]) - - return args - }, []) -}