]> git.immae.eu Git - github/fretlink/purs-loader.git/commitdiff
Add babelified source.
authorAlex Mingoia <talk@alexmingoia.com>
Sat, 14 May 2016 05:35:10 +0000 (22:35 -0700)
committerAlex Mingoia <talk@alexmingoia.com>
Sat, 14 May 2016 05:35:10 +0000 (22:35 -0700)
.gitignore
index.js [new file with mode: 0644]

index 548b3c7fa9e5abe68b6c1713212988b69c889e9c..af15f97449abf3d4dedba169695c5253a9f74bd3 100644 (file)
@@ -2,4 +2,3 @@
 build/
 node_modules/
 bower_components/
-/index.js
diff --git a/index.js b/index.js
new file mode 100644 (file)
index 0000000..b06aed0
--- /dev/null
+++ b/index.js
@@ -0,0 +1,472 @@
+'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;
+  }, []);
+}