]> git.immae.eu Git - github/fretlink/purs-loader.git/commitdiff
Merge remote-tracking branch 'fretlink/master' into latest
authorCyril Sobierajewicz <cyril.sobierajewicz@fretlink.com>
Fri, 22 Mar 2019 16:39:38 +0000 (17:39 +0100)
committerCyril Sobierajewicz <cyril.sobierajewicz@fretlink.com>
Fri, 22 Mar 2019 16:39:38 +0000 (17:39 +0100)
lib/bundle.js [new file with mode: 0644]
lib/compile.js [new file with mode: 0644]
lib/dargs.js [new file with mode: 0644]
lib/ide.js [new file with mode: 0644]
lib/index.js [new file with mode: 0644]
lib/purs-module-map.js [new file with mode: 0644]
lib/source-maps.js [new file with mode: 0644]
lib/to-javascript.js [new file with mode: 0644]
lib/utils.js [new file with mode: 0644]

diff --git a/lib/bundle.js b/lib/bundle.js
new file mode 100644 (file)
index 0000000..943e08b
--- /dev/null
@@ -0,0 +1,63 @@
+'use strict';
+
+var path = require('path');
+
+var Promise = require('bluebird');
+
+var fs = Promise.promisifyAll(require('fs'));
+
+var spawn = require('cross-spawn');
+
+var debug = require('debug')('purs-loader');
+
+var dargs = require('./dargs');
+
+module.exports = function bundle(options, bundleModules) {
+  var stdout = [];
+
+  var stderr = [];
+
+  var bundleCommand = options.pscBundle || 'purs';
+
+  var bundleArgs = (options.pscBundle ? [] : ['bundle']).concat(dargs(Object.assign({
+    _: [path.join(options.output, '*', '*.js')],
+    output: options.bundleOutput,
+    namespace: options.bundleNamespace
+  }, options.pscBundleArgs)));
+
+  bundleModules.forEach(function (name) {
+    return bundleArgs.push('--module', name);
+  });
+
+  debug('bundle: %s %O', bundleCommand, bundleArgs);
+
+  return new Promise(function (resolve, reject) {
+    debug('bundling PureScript...');
+
+    var compilation = spawn(bundleCommand, bundleArgs);
+
+    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) {
+      debug('finished bundling PureScript.');
+
+      if (code !== 0) {
+        var errorMessage = stderr.join('');
+
+        if (errorMessage.length) {
+          psModule.emitError(errorMessage);
+        }
+
+        reject(new Error('bundling failed'));
+      } else {
+        resolve(fs.appendFileAsync(options.bundleOutput, 'module.exports = ' + options.bundleNamespace));
+      }
+    });
+  });
+};
\ No newline at end of file
diff --git a/lib/compile.js b/lib/compile.js
new file mode 100644 (file)
index 0000000..a1bf211
--- /dev/null
@@ -0,0 +1,64 @@
+'use strict';
+
+var Promise = require('bluebird');
+
+var spawn = require('cross-spawn');
+
+var debug_ = require('debug');
+
+var debug = debug_('purs-loader');
+
+var debugVerbose = debug_('purs-loader:verbose');
+
+var dargs = require('./dargs');
+
+module.exports = function compile(psModule) {
+  var options = psModule.options;
+
+  var compileCommand = options.psc || 'purs';
+
+  var compileArgs = (options.psc ? [] : ['compile']).concat(dargs(Object.assign({
+    _: options.src,
+    output: options.output
+  }, options.pscArgs)));
+
+  var stderr = [];
+
+  debug('compile %s %O', compileCommand, compileArgs);
+
+  return new Promise(function (resolve, reject) {
+    debug('compiling PureScript...');
+
+    var compilation = spawn(compileCommand, compileArgs);
+
+    compilation.stderr.on('data', function (data) {
+      stderr.push(data.toString());
+    });
+
+    compilation.stdout.on('data', function (data) {
+      debugVerbose(data.toString());
+    });
+
+    compilation.on('close', function (code) {
+      debug('finished compiling PureScript.');
+
+      if (code !== 0) {
+        var errorMessage = stderr.join('');
+        if (errorMessage.length) {
+          psModule.emitError(errorMessage);
+        }
+        if (options.watch) {
+          resolve(psModule);
+        } else {
+          reject(new Error('compilation failed'));
+        }
+      } else {
+        var warningMessage = stderr.join('');
+        if (options.warnings && warningMessage.length) {
+          psModule.emitWarning(warningMessage);
+        }
+        resolve(psModule);
+      }
+    });
+  });
+};
\ No newline at end of file
diff --git a/lib/dargs.js b/lib/dargs.js
new file mode 100644 (file)
index 0000000..191ab44
--- /dev/null
@@ -0,0 +1,7 @@
+'use strict';
+
+var dargs = require('dargs');
+
+module.exports = function (obj) {
+  return dargs(obj, { ignoreFalse: true });
+};
\ No newline at end of file
diff --git a/lib/ide.js b/lib/ide.js
new file mode 100644 (file)
index 0000000..6087e1c
--- /dev/null
@@ -0,0 +1,270 @@
+'use strict';
+
+var path = require('path');
+
+var Promise = require('bluebird');
+
+var fs = Promise.promisifyAll(require('fs'));
+
+var retryPromise = require('promise-retry');
+
+var spawn = require('cross-spawn');
+
+var colors = require('chalk');
+
+var debug_ = require('debug');
+
+var debug = debug_('purs-loader');
+
+var debugVerbose = debug_('purs-loader:verbose');
+
+var dargs = require('./dargs');
+
+var compile = require('./compile');
+
+var PsModuleMap = require('./purs-module-map');
+
+function UnknownModuleError() {
+  this.name = 'UnknownModuleError';
+  this.stack = new Error().stack;
+}
+
+UnknownModuleError.prototype = Object.create(Error.prototype);
+
+UnknownModuleError.prototype.constructor = UnknownModuleError;
+
+module.exports.UnknownModuleError = UnknownModuleError;
+
+function spawnIdeClient(body, options) {
+  var ideClientCommand = options.pscIdeClient || 'purs';
+
+  var ideClientArgs = (options.pscIdeClient ? [] : ['ide', 'client']).concat(dargs(options.pscIdeClientArgs));
+
+  var stderr = [];
+
+  var stdout = [];
+
+  debug('ide client %s %o %O', ideClientCommand, ideClientArgs, body);
+
+  return new Promise(function (resolve, reject) {
+    var ideClient = spawn(ideClientCommand, ideClientArgs);
+
+    ideClient.stderr.on('data', function (data) {
+      stderr.push(data.toString());
+    });
+
+    ideClient.stdout.on('data', function (data) {
+      stdout.push(data.toString());
+    });
+
+    ideClient.on('close', function (code) {
+      if (code !== 0) {
+        var errorMessage = stderr.join('');
+
+        reject(new Error('ide client failed: ' + errorMessage));
+      } else {
+        var result = stdout.join('');
+
+        resolve(result);
+      }
+    });
+
+    ideClient.stdin.resume();
+
+    ideClient.stdin.write(JSON.stringify(body));
+
+    ideClient.stdin.write('\n');
+  });
+}
+
+function formatIdeResult(result, options, index, length) {
+  var numAndErr = '[' + (index + 1) + '/' + length + ' ' + result.errorCode + ']';
+  numAndErr = options.pscIdeColors ? colors.yellow(numAndErr) : numAndErr;
+
+  function makeResult() {
+    return Promise.resolve('\n' + numAndErr + ' ' + result.message);
+  }
+
+  function makeResultSnippet(filename, pos) {
+    var srcPath = path.relative(options.context, filename);
+    var fileAndPos = srcPath + ':' + pos.startLine + ':' + pos.startColumn;
+
+    return fs.readFileAsync(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);
+    }).catch(function (error) {
+      debug('failed to format ide result: %o', error);
+
+      return Promise.resolve('');
+    });
+  }
+
+  return result.filename && result.position ? makeResultSnippet(result.filename, result.position) : makeResult();
+}
+
+module.exports.connect = function connect(psModule) {
+  var options = psModule.options;
+
+  var serverCommand = options.pscIdeServer || 'purs';
+
+  var serverArgs = (options.pscIdeServer ? [] : ['ide', 'server']).concat(dargs(Object.assign({
+    outputDirectory: options.output,
+    '_': options.src
+  }, options.pscIdeServerArgs)));
+
+  debug('ide server: %s %o', serverCommand, serverArgs);
+
+  var ideServer = spawn(serverCommand, serverArgs);
+
+  ideServer.stdout.on('data', function (data) {
+    debugVerbose('ide server stdout: %s', data.toString());
+  });
+
+  ideServer.stderr.on('data', function (data) {
+    debugVerbose('ide server stderr: %s', data.toString());
+  });
+
+  ideServer.on('error', function (error) {
+    debugVerbose('ide server error: %o', error);
+  });
+
+  ideServer.on('close', function (code, signal) {
+    debugVerbose('ide server close: %s %s', code, signal);
+  });
+
+  return Promise.resolve(ideServer);
+};
+
+module.exports.load = function load(psModule) {
+  var options = psModule.options;
+
+  var body = { command: 'load' };
+
+  return spawnIdeClient(body, options);
+};
+
+module.exports.loadWithRetry = function loadWithRetry(psModule) {
+  var retries = 9;
+
+  return retryPromise(function (retry, number) {
+    debugVerbose('attempting to load modules (%d out of %d attempts)', number, retries);
+
+    return module.exports.load(psModule).catch(retry);
+  }, {
+    retries: retries,
+    factor: 1,
+    minTimeout: 333,
+    maxTimeout: 333
+  }).then(function () {
+    return psModule;
+  });
+};
+
+module.exports.rebuild = function rebuild(psModule) {
+  var options = psModule.options;
+
+  var body = {
+    command: 'rebuild',
+    params: {
+      file: psModule.srcPath
+    }
+  };
+
+  var parseResponse = function parseResponse(response) {
+    try {
+      var parsed = JSON.parse(response);
+
+      debugVerbose('parsed JSON response: %O', parsed);
+
+      return Promise.resolve(parsed);
+    } catch (error) {
+      return Promise.reject(error);
+    }
+  };
+
+  var formatResponse = function formatResponse(parsed) {
+    var result = Array.isArray(parsed.result) ? parsed.result : [];
+
+    return Promise.map(result, function (item, i) {
+      debugVerbose('formatting result %O', item);
+
+      return formatIdeResult(item, options, i, result.length);
+    }).then(function (formatted) {
+      return {
+        parsed: parsed,
+        formatted: formatted,
+        formattedMessage: formatted.join('\n')
+      };
+    });
+  };
+
+  return spawnIdeClient(body, options).then(parseResponse).then(formatResponse).then(function (_ref) {
+    var parsed = _ref.parsed,
+        formatted = _ref.formatted,
+        formattedMessage = _ref.formattedMessage;
+
+    if (parsed.resultType === 'success') {
+      if (options.warnings && formattedMessage.length) {
+        psModule.emitWarning(formattedMessage);
+      }
+
+      return psModule;
+    } else if ((parsed.result || []).some(function (item) {
+      var isModuleNotFound = item.errorCode === 'ModuleNotFound';
+
+      var isUnknownModule = item.errorCode === 'UnknownModule';
+
+      var isUnknownModuleImport = item.errorCode === 'UnknownName' && /Unknown module/.test(item.message);
+
+      return isModuleNotFound || isUnknownModule || isUnknownModuleImport;
+    })) {
+      debug('module %s was not rebuilt because the module is unknown', psModule.name);
+
+      return Promise.reject(new UnknownModuleError());
+    } else {
+      if (formattedMessage.length) {
+        psModule.emitError(formattedMessage);
+      }
+
+      return psModule;
+    }
+  });
+};
\ No newline at end of file
diff --git a/lib/index.js b/lib/index.js
new file mode 100644 (file)
index 0000000..c6e88f8
--- /dev/null
@@ -0,0 +1,463 @@
+'use strict';
+
+var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }();
+
+var debug_ = require('debug');
+
+var debug = debug_('purs-loader');
+
+var debugVerbose = debug_('purs-loader:verbose');
+
+var loaderUtils = require('loader-utils');
+
+var Promise = require('bluebird');
+
+var path = require('path');
+
+var PsModuleMap = require('./purs-module-map');
+
+var compile = require('./compile');
+
+var bundle = require('./bundle');
+
+var ide = require('./ide');
+
+var toJavaScript = require('./to-javascript');
+
+var sourceMaps = require('./source-maps');
+
+var dargs = require('./dargs');
+
+var utils = require('./utils');
+
+var spawn = require('cross-spawn').sync;
+
+var eol = require('os').EOL;
+
+var CACHE_VAR = {
+  rebuild: false,
+  deferred: [],
+  bundleModules: [],
+  ideServer: null,
+  psModuleMap: null,
+  warnings: [],
+  errors: [],
+  compilationStarted: false,
+  compilationFinished: false,
+  compilationFailed: false,
+  installed: false,
+  srcOption: []
+};
+
+module.exports = function purescriptLoader(source, map) {
+  var _this = this;
+
+  this.cacheable && this.cacheable();
+
+  var webpackContext = this.options && this.options.context || this.rootContext;
+
+  var callback = this.async();
+
+  var loaderOptions = loaderUtils.getOptions(this) || {};
+
+  var srcOption = function (pscPackage) {
+    var srcPath = path.join('src', '**', '*.purs');
+
+    var bowerPath = path.join('bower_components', 'purescript-*', 'src', '**', '*.purs');
+
+    if (CACHE_VAR.srcOption.length > 0) {
+      return CACHE_VAR.srcOption;
+    } else if (pscPackage) {
+      var pscPackageCommand = 'psc-package';
+
+      var pscPackageArgs = ['sources'];
+
+      var loaderSrc = loaderOptions.src || [srcPath];
+
+      debug('psc-package %s %o', pscPackageCommand, pscPackageArgs);
+
+      var cmd = spawn(pscPackageCommand, pscPackageArgs);
+
+      if (cmd.error) {
+        throw new Error(cmd.error);
+      } else if (cmd.status !== 0) {
+        var error = cmd.stdout.toString();
+
+        throw new Error(error);
+      } else {
+        var result = cmd.stdout.toString().split(eol).filter(function (v) {
+          return v != '';
+        }).concat(loaderSrc);
+
+        debug('psc-package result: %o', result);
+
+        CACHE_VAR.srcOption = result;
+
+        return result;
+      }
+    } else {
+      var _result = loaderOptions.src || [bowerPath, srcPath];
+
+      CACHE_VAR.srcOption = _result;
+
+      return _result;
+    }
+  }(loaderOptions.pscPackage);
+
+  var options = Object.assign({
+    context: webpackContext,
+    psc: null,
+    pscArgs: {},
+    pscBundle: null,
+    pscBundleArgs: {},
+    pscIdeClient: null,
+    pscIdeClientArgs: {},
+    pscIdeServer: null,
+    pscIdeServerArgs: {},
+    pscIde: false,
+    pscIdeColors: loaderOptions.psc === 'psa',
+    pscPackage: false,
+    bundleOutput: 'output/bundle.js',
+    bundleNamespace: 'PS',
+    bundle: false,
+    warnings: true,
+    watch: false,
+    output: 'output',
+    src: []
+  }, loaderOptions, {
+    src: srcOption
+  });
+
+  if (!CACHE_VAR.installed) {
+    debugVerbose('installing purs-loader with options: %O', options);
+
+    CACHE_VAR.installed = true;
+
+    // invalidate loader CACHE_VAR when bundle is marked as invalid (in watch mode)
+    this._compiler.plugin('invalid', function () {
+      debugVerbose('invalidating loader CACHE_VAR');
+
+      CACHE_VAR = {
+        rebuild: options.pscIde,
+        deferred: [],
+        bundleModules: [],
+        ideServer: CACHE_VAR.ideServer,
+        psModuleMap: CACHE_VAR.psModuleMap,
+        warnings: [],
+        errors: [],
+        compilationStarted: false,
+        compilationFinished: false,
+        compilationFailed: false,
+        installed: CACHE_VAR.installed,
+        srcOption: []
+      };
+    });
+
+    // add psc warnings to webpack compilation warnings
+    this._compiler.plugin('after-compile', function (compilation, callback) {
+      CACHE_VAR.warnings.forEach(function (warning) {
+        compilation.warnings.push(warning);
+      });
+
+      CACHE_VAR.errors.forEach(function (error) {
+        compilation.errors.push(error);
+      });
+
+      callback();
+    });
+  }
+
+  var psModuleName = PsModuleMap.matchModule(source);
+
+  var psModule = {
+    name: psModuleName,
+    source: source,
+    load: function load(_ref) {
+      var js = _ref.js,
+          map = _ref.map;
+      return callback(null, js, map);
+    },
+    reject: function reject(error) {
+      return callback(error);
+    },
+    srcPath: this.resourcePath,
+    remainingRequest: loaderUtils.getRemainingRequest(this),
+    srcDir: path.dirname(this.resourcePath),
+    jsPath: path.resolve(path.join(options.output, psModuleName, 'index.js')),
+    options: options,
+    cache: CACHE_VAR,
+    emitWarning: function emitWarning(warning) {
+      if (options.warnings && warning.length) {
+        CACHE_VAR.warnings.push(warning);
+      }
+    },
+    emitError: function emitError(pscMessage) {
+      if (pscMessage.length) {
+        var modules = [];
+
+        var matchErrorsSeparator = /\n(?=Error)/;
+        var errors = pscMessage.split(matchErrorsSeparator);
+        var _iteratorNormalCompletion = true;
+        var _didIteratorError = false;
+        var _iteratorError = undefined;
+
+        try {
+          for (var _iterator = errors[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) {
+            var error = _step.value;
+
+            var matchErrLocation = /at (.+\.purs):(\d+):(\d+) - (\d+):(\d+) \(line \2, column \3 - line \4, column \5\)/;
+
+            var _ref2 = matchErrLocation.exec(error) || [],
+                _ref3 = _slicedToArray(_ref2, 2),
+                filename = _ref3[1];
+
+            if (!filename) continue;
+
+            var baseModulePath = path.join(_this.rootContext, filename);
+            _this.addDependency(baseModulePath);
+
+            var foreignModulesErrorCodes = ['ErrorParsingFFIModule', 'MissingFFIImplementations', 'UnusedFFIImplementations', 'MissingFFIModule'];
+            var _iteratorNormalCompletion2 = true;
+            var _didIteratorError2 = false;
+            var _iteratorError2 = undefined;
+
+            try {
+              for (var _iterator2 = foreignModulesErrorCodes[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) {
+                var code = _step2.value;
+
+                if (error.includes(code)) {
+                  var resolved = utils.resolveForeignModule(baseModulePath);
+                  _this.addDependency(resolved);
+                }
+              }
+            } catch (err) {
+              _didIteratorError2 = true;
+              _iteratorError2 = err;
+            } finally {
+              try {
+                if (!_iteratorNormalCompletion2 && _iterator2.return) {
+                  _iterator2.return();
+                }
+              } finally {
+                if (_didIteratorError2) {
+                  throw _iteratorError2;
+                }
+              }
+            }
+
+            var matchErrModuleName = /in module ((?:\w+\.)*\w+)/;
+
+            var _ref4 = matchErrModuleName.exec(error) || [],
+                _ref5 = _slicedToArray(_ref4, 2),
+                baseModuleName = _ref5[1];
+
+            if (!baseModuleName) continue;
+
+            var matchMissingModuleName = /Module ((?:\w+\.)*\w+) was not found/;
+            var matchMissingImportFromModuleName = /Cannot import value \w+ from module ((?:\w+\.)*\w+)/;
+            var _arr = [matchMissingModuleName, matchMissingImportFromModuleName];
+            for (var _i = 0; _i < _arr.length; _i++) {
+              var re = _arr[_i];
+              var _ref6 = re.exec(error) || [],
+                  _ref7 = _slicedToArray(_ref6, 2),
+                  targetModuleName = _ref7[1];
+
+              if (targetModuleName) {
+                var _resolved = utils.resolvePursModule({
+                  baseModulePath: baseModulePath,
+                  baseModuleName: baseModuleName,
+                  targetModuleName: targetModuleName
+                });
+                _this.addDependency(_resolved);
+              }
+            }
+
+            var desc = {
+              name: baseModuleName,
+              filename: baseModulePath
+            };
+
+            if (typeof _this.describePscError === 'function') {
+              var _describePscError = _this.describePscError(error, desc),
+                  _describePscError$dep = _describePscError.dependencies,
+                  dependencies = _describePscError$dep === undefined ? [] : _describePscError$dep,
+                  details = _describePscError.details;
+
+              var _iteratorNormalCompletion3 = true;
+              var _didIteratorError3 = false;
+              var _iteratorError3 = undefined;
+
+              try {
+
+                for (var _iterator3 = dependencies[Symbol.iterator](), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) {
+                  var dep = _step3.value;
+
+                  _this.addDependency(dep);
+                }
+              } catch (err) {
+                _didIteratorError3 = true;
+                _iteratorError3 = err;
+              } finally {
+                try {
+                  if (!_iteratorNormalCompletion3 && _iterator3.return) {
+                    _iterator3.return();
+                  }
+                } finally {
+                  if (_didIteratorError3) {
+                    throw _iteratorError3;
+                  }
+                }
+              }
+
+              Object.assign(desc, details);
+            }
+
+            modules.push(desc);
+          }
+        } catch (err) {
+          _didIteratorError = true;
+          _iteratorError = err;
+        } finally {
+          try {
+            if (!_iteratorNormalCompletion && _iterator.return) {
+              _iterator.return();
+            }
+          } finally {
+            if (_didIteratorError) {
+              throw _iteratorError;
+            }
+          }
+        }
+
+        CACHE_VAR.errors.push(new utils.PscError(pscMessage, modules));
+      }
+    }
+  };
+
+  debug('loading %s', psModule.name);
+
+  if (options.bundle) {
+    CACHE_VAR.bundleModules.push(psModule.name);
+  }
+
+  if (CACHE_VAR.rebuild) {
+    var connect = function connect() {
+      if (!CACHE_VAR.ideServer) {
+        CACHE_VAR.ideServer = true;
+
+        return ide.connect(psModule).then(function (ideServer) {
+          CACHE_VAR.ideServer = ideServer;
+          return psModule;
+        }).then(ide.loadWithRetry).catch(function (error) {
+          if (CACHE_VAR.ideServer.kill) {
+            debug('ide failed to initially load modules, stopping the ide server process');
+
+            CACHE_VAR.ideServer.kill();
+          }
+
+          CACHE_VAR.ideServer = null;
+
+          return Promise.reject(error);
+        });
+      } else {
+        return Promise.resolve(psModule);
+      }
+    };
+
+    var rebuild = function rebuild() {
+      return ide.rebuild(psModule).then(function () {
+        return toJavaScript(psModule).then(function (js) {
+          return sourceMaps(psModule, js);
+        }).then(psModule.load).catch(psModule.reject);
+      }).catch(function (error) {
+        if (error instanceof ide.UnknownModuleError) {
+          // Store the modules that trigger a recompile due to an
+          // unknown module error. We need to wait until compilation is
+          // done before loading these files.
+
+          CACHE_VAR.deferred.push(psModule);
+
+          if (!CACHE_VAR.compilationStarted) {
+            CACHE_VAR.compilationStarted = true;
+
+            return compile(psModule).then(function () {
+              CACHE_VAR.compilationFinished = true;
+            }).then(function () {
+              return Promise.map(CACHE_VAR.deferred, function (psModule) {
+                return ide.load(psModule).then(function () {
+                  return toJavaScript(psModule);
+                }).then(function (js) {
+                  return sourceMaps(psModule, js);
+                }).then(psModule.load);
+              });
+            }).catch(function (error) {
+              CACHE_VAR.compilationFailed = true;
+
+              CACHE_VAR.deferred[0].reject(error);
+
+              CACHE_VAR.deferred.slice(1).forEach(function (psModule) {
+                psModule.reject(new Error('purs-loader failed'));
+              });
+            });
+          } else if (CACHE_VAR.compilationFailed) {
+            CACHE_VAR.deferred.pop().reject(new Error('purs-loader failed'));
+          } else {
+            // The compilation has started. We must wait until it is
+            // done in order to ensure the module map contains all of
+            // the unknown modules.
+          }
+        } else {
+          debug('ide rebuild failed due to an unhandled error: %o', error);
+
+          psModule.reject(error);
+        }
+      });
+    };
+
+    connect().then(rebuild);
+  } else if (CACHE_VAR.compilationFinished) {
+    debugVerbose('compilation is already finished, loading module %s', psModule.name);
+
+    toJavaScript(psModule).then(function (js) {
+      return sourceMaps(psModule, js);
+    }).then(psModule.load).catch(psModule.reject);
+  } else {
+    // The compilation has not finished yet. We need to wait for
+    // compilation to finish before the loaders run so that references
+    // to compiled output are valid. Push the modules into the CACHE_VAR to
+    // be loaded once the complation is complete.
+
+    CACHE_VAR.deferred.push(psModule);
+
+    if (!CACHE_VAR.compilationStarted) {
+      CACHE_VAR.compilationStarted = true;
+
+      compile(psModule).then(function () {
+        CACHE_VAR.compilationFinished = true;
+      }).then(function () {
+        if (options.bundle) {
+          return bundle(options, CACHE_VAR.bundleModules);
+        }
+      }).then(function () {
+        return Promise.map(CACHE_VAR.deferred, function (psModule) {
+          return toJavaScript(psModule).then(function (js) {
+            return sourceMaps(psModule, js);
+          }).then(psModule.load);
+        });
+      }).catch(function (error) {
+        CACHE_VAR.compilationFailed = true;
+
+        CACHE_VAR.deferred[0].reject(error);
+
+        CACHE_VAR.deferred.slice(1).forEach(function (psModule) {
+          psModule.reject(new Error('purs-loader failed'));
+        });
+      });
+    } else if (CACHE_VAR.compilationFailed) {
+      CACHE_VAR.deferred.pop().reject(new Error('purs-loader failed'));
+    } else {
+      // The complation has started. Nothing to do but wait until it is
+      // done before loading all of the modules.
+    }
+  }
+};
\ No newline at end of file
diff --git a/lib/purs-module-map.js b/lib/purs-module-map.js
new file mode 100644 (file)
index 0000000..cb06322
--- /dev/null
@@ -0,0 +1,78 @@
+'use strict';
+
+var path = require('path');
+
+var Promise = require('bluebird');
+
+var fs = Promise.promisifyAll(require('fs'));
+
+var globby = require('globby');
+
+var debug = require('debug')('purs-loader');
+
+var srcModuleRegex = /(?:^|\n)module\s+([\w\.]+)/i;
+
+var importModuleRegex = /(?:^|\n)\s*import\s+([\w\.]+)/ig;
+
+module.exports.matchModule = function matchModule(str) {
+  var matches = str.match(srcModuleRegex);
+  return matches && matches[1];
+};
+
+module.exports.matchImports = function matchImports(str) {
+  var matches = str.match(importModuleRegex);
+  return (matches || []).map(function (a) {
+    return a.replace(/\n?\s*import\s+/i, '');
+  });
+};
+
+module.exports.makeMapEntry = function makeMapEntry(filePurs) {
+  var dirname = path.dirname(filePurs);
+
+  var basename = path.basename(filePurs, '.purs');
+
+  var fileJs = path.join(dirname, basename + '.js');
+
+  var result = Promise.props({
+    filePurs: fs.readFileAsync(filePurs, 'utf8'),
+    fileJs: fs.readFileAsync(fileJs, 'utf8').catch(function () {
+      return undefined;
+    })
+  }).then(function (fileMap) {
+    var sourcePurs = fileMap.filePurs;
+
+    var sourceJs = fileMap.fileJs;
+
+    var moduleName = module.exports.matchModule(sourcePurs);
+
+    var imports = module.exports.matchImports(sourcePurs);
+
+    var map = {};
+
+    map[moduleName] = map[moduleName] || {};
+
+    map[moduleName].src = path.resolve(filePurs);
+
+    map[moduleName].imports = imports;
+
+    if (sourceJs) {
+      map[moduleName].ffi = path.resolve(fileJs);
+    }
+
+    return map;
+  });
+
+  return result;
+};
+
+module.exports.makeMap = function makeMap(src) {
+  debug('loading PureScript source and FFI files from %o', src);
+
+  var globs = [].concat(src);
+
+  return globby(globs).then(function (paths) {
+    return Promise.all(paths.map(module.exports.makeMapEntry)).then(function (result) {
+      return result.reduce(Object.assign, {});
+    });
+  });
+};
\ No newline at end of file
diff --git a/lib/source-maps.js b/lib/source-maps.js
new file mode 100644 (file)
index 0000000..3ad70dd
--- /dev/null
@@ -0,0 +1,61 @@
+'use strict';
+
+var Promise = require('bluebird');
+
+var fs = require('fs');
+
+var path = require('path');
+
+var debug_ = require('debug');
+
+var debugVerbose = debug_('purs-loader:verbose');
+
+module.exports = function sourceMap(psModule, js) {
+  var options = psModule.options;
+
+  var jsPath = psModule.jsPath;
+
+  var srcPath = psModule.srcPath;
+
+  var source = psModule.source;
+
+  var remainingRequest = psModule.remainingRequest;
+
+  var sourceMapPath = path.join(path.dirname(jsPath), 'index.js.map');
+
+  var isSourceMapsEnabled = options.pscArgs && options.pscArgs.sourceMaps;
+
+  return new Promise(function (resolve, reject) {
+    if (!isSourceMapsEnabled) {
+      resolve({
+        js: js,
+        map: undefined
+      });
+    } else {
+      debugVerbose('loading source map %s', sourceMapPath);
+
+      fs.readFile(sourceMapPath, 'utf-8', function (error, result) {
+        if (error) {
+          reject(error);
+        } else {
+          try {
+            var map = Object.assign(JSON.parse(result), {
+              sources: [remainingRequest],
+              file: path.normalize(srcPath),
+              sourcesContent: [source]
+            });
+
+            var jsRemovedMapUrl = js.replace(/^\/\/# sourceMappingURL=[^\r\n]*/gm, '');
+
+            resolve({
+              js: jsRemovedMapUrl,
+              map: map
+            });
+          } catch (error) {
+            reject(error);
+          }
+        }
+      });
+    }
+  });
+};
\ No newline at end of file
diff --git a/lib/to-javascript.js b/lib/to-javascript.js
new file mode 100644 (file)
index 0000000..ce49704
--- /dev/null
@@ -0,0 +1,152 @@
+'use strict';
+
+var Promise = require('bluebird');
+
+var fs = Promise.promisifyAll(require('fs'));
+
+var path = require('path');
+
+var jsStringEscape = require('js-string-escape');
+
+var difference = require('lodash.difference');
+
+var debug_ = require('debug');
+
+var debug = debug_('purs-loader');
+
+var debugVerbose = debug_('purs-loader:verbose');
+
+var PsModuleMap = require('./purs-module-map');
+
+function updatePsModuleMap(psModule) {
+  var options = psModule.options;
+
+  var cache = psModule.cache;
+
+  var filePurs = psModule.srcPath;
+
+  if (!cache.psModuleMap) {
+    debugVerbose('module mapping does not exist - making a new module map');
+
+    cache.psModuleMap = PsModuleMap.makeMap(options.src);
+
+    return cache.psModuleMap;
+  } else {
+    debugVerbose('module mapping exists - updating module map for %s', filePurs);
+
+    cache.psModuleMap = cache.psModuleMap.then(function (psModuleMap) {
+      return PsModuleMap.makeMapEntry(filePurs).then(function (result) {
+        var map = Object.assign(psModuleMap, result);
+
+        return map;
+      });
+    });
+
+    return cache.psModuleMap;
+  }
+}
+
+// Reference the bundle.
+function makeBundleJS(psModule) {
+  var bundleOutput = psModule.options.bundleOutput;
+
+  var name = psModule.name;
+
+  var srcDir = psModule.srcDir;
+
+  var escaped = jsStringEscape(path.relative(srcDir, bundleOutput));
+
+  var result = 'module.exports = require("' + escaped + '")["' + name + '"]';
+
+  return Promise.resolve(result);
+}
+
+// Replace require paths to output files generated by psc with paths
+// to purescript sources, which are then also run through this loader.
+// Additionally, the imports replaced are tracked so that in the event
+// the compiler fails to compile the PureScript source, we can tack on
+// any new imports in order to allow webpack to watch the new files
+// before they have been successfully compiled.
+function makeJS(psModule, psModuleMap, js) {
+  var requireRE = /require\(['"]\.\.\/([\w\.]+)(?:\/index\.js)?['"]\)/g;
+
+  var foreignRE = /require\(['"]\.\/foreign(?:\.js)?['"]\)/g;
+
+  var name = psModule.name;
+
+  var imports = psModuleMap[name].imports;
+
+  var replacedImports = [];
+
+  var result = js.replace(requireRE, function (m, p1) {
+    var moduleValue = psModuleMap[p1];
+
+    if (!moduleValue) {
+      debug('module %s was not found in the map, replacing require with null', p1);
+
+      return 'null';
+    } else {
+      var escapedPath = jsStringEscape(moduleValue.src);
+
+      replacedImports.push(p1);
+
+      return 'require("' + escapedPath + '")';
+    }
+  }).replace(foreignRE, function () {
+    var escapedPath = jsStringEscape(psModuleMap[name].ffi);
+
+    return 'require("' + escapedPath + '")';
+  });
+
+  var additionalImports = difference(imports, replacedImports);
+
+  if (!additionalImports.length) {
+    return Promise.resolve(result);
+  } else {
+    debug('rebuilding module map due to additional imports for %s: %o', name, additionalImports);
+
+    psModule.cache.psModuleMap = null;
+
+    return updatePsModuleMap(psModule).then(function (updatedPsModuleMap) {
+      var additionalImportsResult = additionalImports.map(function (import_) {
+        var moduleValue = updatedPsModuleMap[import_];
+
+        if (!moduleValue) {
+          debug('module %s was not found in the map, skipping require', import_);
+
+          return null;
+        } else {
+          var escapedPath = jsStringEscape(moduleValue.src);
+
+          return 'var ' + import_.replace(/\./g, '_') + ' = require("' + escapedPath + '")';
+        }
+      }).filter(function (a) {
+        return a !== null;
+      }).join('\n');
+
+      return result + '\n' + additionalImportsResult;
+    });
+  }
+}
+
+module.exports = function toJavaScript(psModule) {
+  var options = psModule.options;
+
+  var cache = psModule.cache;
+
+  var bundlePath = path.resolve(options.bundleOutput);
+
+  var jsPath = options.bundle ? bundlePath : psModule.jsPath;
+
+  var js = fs.readFileAsync(jsPath, 'utf8').catch(function () {
+    return '';
+  });
+
+  var psModuleMap = updatePsModuleMap(psModule);
+
+  debugVerbose('loading JavaScript for %s', psModule.name);
+
+  return Promise.props({ js: js, psModuleMap: psModuleMap }).then(function (result) {
+    return options.bundle ? makeBundleJS(psModule) : makeJS(psModule, result.psModuleMap, result.js);
+  });
+};
\ No newline at end of file
diff --git a/lib/utils.js b/lib/utils.js
new file mode 100644 (file)
index 0000000..3c73fc6
--- /dev/null
@@ -0,0 +1,68 @@
+'use strict';
+
+var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
+
+function _toArray(arr) { return Array.isArray(arr) ? arr : Array.from(arr); }
+
+function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } }
+
+function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
+
+function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
+
+function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
+
+var path = require('path');
+
+exports.PscError = function (_Error) {
+  _inherits(PscError, _Error);
+
+  function PscError(message, modules) {
+    _classCallCheck(this, PscError);
+
+    var _this = _possibleConstructorReturn(this, (PscError.__proto__ || Object.getPrototypeOf(PscError)).call(this, message));
+
+    _this.modules = modules;
+    _this.isPscError = true;
+    return _this;
+  }
+
+  _createClass(PscError, null, [{
+    key: 'name',
+    get: function get() {
+      return 'PscError';
+    }
+  }]);
+
+  return PscError;
+}(Error);
+
+var repeat = function repeat(value, times) {
+  return times <= 0 ? [] : [value].concat(_toConsumableArray(repeat(value, times - 1)));
+};
+var diffPursModuleNames = function diffPursModuleNames(from, target, parts) {
+  if (!from.length) return parts.concat(target);
+  if (!target.length) return parts.concat(repeat('..', from.length));
+
+  var _from = _toArray(from),
+      head_from = _from[0],
+      tail_from = _from.slice(1);
+
+  var _target = _toArray(target),
+      head_target = _target[0],
+      tail_target = _target.slice(1);
+
+  return head_from === head_target ? diffPursModuleNames(tail_from, tail_target, parts) : parts.concat(repeat('..', from.length), target);
+};
+exports.resolvePursModule = function (_ref) {
+  var baseModulePath = _ref.baseModulePath,
+      baseModuleName = _ref.baseModuleName,
+      targetModuleName = _ref.targetModuleName;
+
+  var parts = diffPursModuleNames(baseModuleName.split('.'), targetModuleName.split('.'), []);
+  return parts.length ? path.resolve(baseModulePath, path.join.apply(path, _toConsumableArray(parts)) + '.purs') : baseModulePath;
+};
+
+exports.resolveForeignModule = function (pursModulePath) {
+  return path.join(path.dirname(pursModulePath), path.basename(pursModulePath, path.extname(pursModulePath)) + '.js');
+};
\ No newline at end of file