--- /dev/null
+'use strict';
+
+var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol ? "symbol" : typeof obj; };
+
+var colors = require('chalk');
+var debug = require('debug')('purs-loader');
+var loaderUtils = require('loader-utils');
+var globby = require('globby');
+var Promise = require('bluebird');
+var fs = Promise.promisifyAll(require('fs'));
+var spawn = require('child_process').spawn;
+var path = require('path');
+var retryPromise = require('promise-retry');
+
+var psModuleRegex = /(?:^|\n)module\s+([\w\.]+)/i;
+var requireRegex = /require\(['"]\.\.\/([\w\.]+)['"]\)/g;
+
+module.exports = function purescriptLoader(source, map) {
+ var callback = this.async();
+ var config = this.options;
+ var query = loaderUtils.parseQuery(this.query);
+ var webpackOptions = this.options.purescriptLoader || {};
+
+ var options = Object.assign({
+ context: config.context,
+ psc: 'psc',
+ pscArgs: {},
+ pscBundle: 'psc-bundle',
+ pscBundleArgs: {},
+ pscIde: false,
+ pscIdeColors: webpackOptions.psc === 'psa' || query.psc === 'psa',
+ pscIdeArgs: {},
+ bundleOutput: 'output/bundle.js',
+ bundleNamespace: 'PS',
+ bundle: false,
+ warnings: true,
+ output: 'output',
+ 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();
+
+ var cache = config.purescriptLoaderCache = config.purescriptLoaderCache || {
+ rebuild: false,
+ deferred: [],
+ bundleModules: []
+ };
+
+ if (!config.purescriptLoaderInstalled) {
+ config.purescriptLoaderInstalled = true;
+
+ // invalidate loader cache when bundle is marked as invalid (in watch mode)
+ this._compiler.plugin('invalid', function () {
+ cache = config.purescriptLoaderCache = {
+ rebuild: options.pscIde,
+ deferred: [],
+ ideServer: cache.ideServer
+ };
+ });
+
+ // add psc warnings to webpack compilation warnings
+ this._compiler.plugin('after-compile', function (compilation, callback) {
+ if (options.warnings && cache.warnings && cache.warnings.length) {
+ compilation.warnings.unshift('PureScript compilation:\n' + cache.warnings.join(''));
+ }
+
+ if (cache.errors && cache.errors.length) {
+ compilation.errors.unshift('PureScript compilation:\n' + cache.errors.join('\n'));
+ }
+
+ callback();
+ });
+ }
+
+ var psModuleName = match(psModuleRegex, source);
+ var psModule = {
+ name: psModuleName,
+ load: function load(js) {
+ return callback(null, js);
+ },
+ reject: function reject(error) {
+ return callback(error);
+ },
+ srcPath: this.resourcePath,
+ srcDir: path.dirname(this.resourcePath),
+ jsPath: path.resolve(path.join(options.output, psModuleName, 'index.js')),
+ options: options,
+ cache: cache
+ };
+
+ if (options.bundle) {
+ cache.bundleModules.push(psModule.name);
+ }
+
+ if (cache.rebuild) {
+ return connectIdeServer(psModule).then(rebuild).then(toJavaScript).then(psModule.load).catch(psModule.reject);
+ }
+
+ if (cache.compilation && cache.compilation.length) {
+ return toJavaScript(psModule).then(psModule.load).catch(psModule.reject);
+ }
+
+ // We need to wait for compilation to finish before the loaders run so that
+ // references to compiled output are valid.
+ cache.deferred.push(psModule);
+
+ if (!cache.compilation) {
+ return compile(psModule).then(function () {
+ return Promise.map(cache.deferred, function (psModule) {
+ if (_typeof(cache.ideServer) === 'object') cache.ideServer.kill();
+ return toJavaScript(psModule).then(psModule.load);
+ });
+ }).catch(function (error) {
+ cache.deferred[0].reject(error);
+ cache.deferred.slice(1).forEach(function (psModule) {
+ return psModule.reject(true);
+ });
+ });
+ }
+};
+
+// The actual loader is executed *after* purescript compilation.
+function toJavaScript(psModule) {
+ var options = psModule.options;
+ var cache = psModule.cache;
+ var bundlePath = path.resolve(options.bundleOutput);
+ var jsPath = cache.bundle ? bundlePath : psModule.jsPath;
+
+ debug('loading JavaScript for', psModule.srcPath);
+
+ return Promise.props({
+ js: fs.readFileAsync(jsPath, 'utf8'),
+ psModuleMap: psModuleMap(options.src, cache)
+ }).then(function (result) {
+ var js = '';
+
+ if (options.bundle) {
+ // if bundling, return a reference to the bundle
+ js = 'module.exports = require("' + 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.
+ var foreignRequire = 'require("' + path.resolve(path.join(psModule.options.output, psModule.name, 'foreign.js')) + '")';
+
+ js = result.js.replace(requireRegex, function (m, p1) {
+ return 'require("' + result.psModuleMap[p1] + '")';
+ }).replace(/require\(['"]\.\/foreign['"]\)/g, foreignRequire);
+ }
+
+ return js;
+ });
+}
+
+function compile(psModule) {
+ var options = psModule.options;
+ var cache = psModule.cache;
+ var stderr = [];
+
+ if (cache.compilation) return Promise.resolve(cache.compilation);
+
+ cache.compilation = [];
+ cache.warnings = [];
+ cache.errors = [];
+
+ var 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(function (resolve, reject) {
+ console.log('\nCompiling PureScript...');
+
+ var compilation = spawn(options.psc, args);
+
+ compilation.stderr.on('data', function (data) {
+ return stderr.push(data.toString());
+ });
+
+ compilation.on('close', function (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(function (compilerOutput) {
+ if (options.bundle) {
+ return bundle(options, cache).then(function () {
+ return psModule;
+ });
+ }
+ return psModule;
+ });
+}
+
+function rebuild(psModule) {
+ var options = psModule.options;
+ var cache = psModule.cache;
+
+ debug('attempting rebuild with psc-ide-client %s', psModule.srcPath);
+
+ var request = function request(body) {
+ return new Promise(function (resolve, reject) {
+ var args = dargs(options.pscIdeArgs);
+ var ideClient = spawn('psc-ide-client', args);
+
+ ideClient.stdout.once('data', function (data) {
+ var res = null;
+
+ try {
+ res = JSON.parse(data.toString());
+ debug(res);
+ } catch (err) {
+ return reject(err);
+ }
+
+ if (res && !Array.isArray(res.result)) {
+ return res.resultType === 'success' ? resolve(psModule) : reject('psc-ide rebuild failed');
+ }
+
+ Promise.map(res.result, function (item, i) {
+ debug(item);
+ return formatIdeResult(item, options, i, res.result.length);
+ }).then(function (compileMessages) {
+ if (res.resultType === 'error') {
+ if (res.result.some(function (item) {
+ return item.errorCode === 'UnknownModule';
+ })) {
+ console.log('Unknown module, attempting full recompile');
+ return compile(psModule).then(function () {
+ return request({ command: 'load' });
+ }).then(resolve).catch(function () {
+ return reject('psc-ide rebuild failed');
+ });
+ }
+ cache.errors = compileMessages;
+ reject('psc-ide rebuild failed');
+ } else {
+ cache.warnings = compileMessages;
+ resolve(psModule);
+ }
+ });
+ });
+
+ ideClient.stderr.once('data', function (data) {
+ return reject(data.toString());
+ });
+
+ ideClient.stdin.write(JSON.stringify(body));
+ ideClient.stdin.write('\n');
+ });
+ };
+
+ return request({
+ command: 'rebuild',
+ params: {
+ file: psModule.srcPath
+ }
+ });
+}
+
+function formatIdeResult(result, options, index, length) {
+ var srcPath = path.relative(options.context, result.filename);
+ var pos = result.position;
+ var fileAndPos = srcPath + ':' + pos.startLine + ':' + pos.startColumn;
+ var numAndErr = '[' + (index + 1) + '/' + length + ' ' + result.errorCode + ']';
+ numAndErr = options.pscIdeColors ? colors.yellow(numAndErr) : numAndErr;
+
+ return fs.readFileAsync(result.filename, 'utf8').then(function (source) {
+ var lines = source.split('\n').slice(pos.startLine - 1, pos.endLine);
+ var endsOnNewline = pos.endColumn === 1 && pos.startLine !== pos.endLine;
+ var up = options.pscIdeColors ? colors.red('^') : '^';
+ var down = options.pscIdeColors ? colors.red('v') : 'v';
+ var 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(function (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;
+ }
+
+ var spaces = ' '.repeat(String(pos.endLine).length);
+ var snippet = trimmed.map(function (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);
+
+ var stdout = [];
+ var stderr = cache.bundle = [];
+
+ var args = dargs(Object.assign({
+ _: [path.join(options.output, '*', '*.js')],
+ output: options.bundleOutput,
+ namespace: options.bundleNamespace
+ }, options.pscBundleArgs));
+
+ cache.bundleModules.forEach(function (name) {
+ return args.push('--module', name);
+ });
+
+ debug('spawning bundler %s %o', options.pscBundle, args.join(' '));
+
+ return new Promise(function (resolve, reject) {
+ console.log('Bundling PureScript...');
+
+ var compilation = spawn(options.pscBundle, args);
+
+ compilation.stdout.on('data', function (data) {
+ return stdout.push(data.toString());
+ });
+ compilation.stderr.on('data', function (data) {
+ return stderr.push(data.toString());
+ });
+ compilation.on('close', function (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(function (paths) {
+ return Promise.props(paths.reduce(function (map, file) {
+ map[file] = fs.readFileAsync(file, 'utf8');
+ return map;
+ }, {})).then(function (srcMap) {
+ cache.psModuleMap = Object.keys(srcMap).reduce(function (map, file) {
+ var source = srcMap[file];
+ var psModuleName = match(psModuleRegex, source);
+ map[psModuleName] = path.resolve(file);
+ return map;
+ }, {});
+ return cache.psModuleMap;
+ });
+ });
+}
+
+function connectIdeServer(psModule) {
+ var options = psModule.options;
+ var cache = psModule.cache;
+
+ if (cache.ideServer) return Promise.resolve(psModule);
+
+ cache.ideServer = true;
+
+ var connect = function connect() {
+ return new Promise(function (resolve, reject) {
+ var args = dargs(options.pscIdeArgs);
+
+ debug('attempting to connect to psc-ide-server', args);
+
+ var ideClient = spawn('psc-ide-client', args);
+
+ ideClient.stderr.on('data', function (data) {
+ debug(data.toString());
+ cache.ideServer = false;
+ reject(true);
+ });
+ ideClient.stdout.once('data', function (data) {
+ debug(data.toString());
+ if (data.toString()[0] === '{') {
+ var 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');
+ });
+ };
+
+ var args = dargs(Object.assign({
+ outputDirectory: options.output
+ }, options.pscIdeArgs));
+
+ debug('attempting to start psc-ide-server', args);
+
+ var ideServer = cache.ideServer = spawn('psc-ide-server', []);
+ ideServer.stderr.on('data', function (data) {
+ debug(data.toString());
+ });
+
+ return retryPromise(function (retry, number) {
+ return connect().catch(function (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) {
+ var matches = str.match(regex);
+ return matches && matches[1];
+}
+
+function dargs(obj) {
+ return Object.keys(obj).reduce(function (args, key) {
+ var arg = '--' + key.replace(/[A-Z]/g, '-$&').toLowerCase();
+ var val = obj[key];
+
+ if (key === '_') val.forEach(function (v) {
+ return args.push(v);
+ });else if (Array.isArray(val)) val.forEach(function (v) {
+ return args.push(arg, v);
+ });else args.push(arg, obj[key]);
+
+ return args;
+ }, []);
+}