index.js 6.73 KB
var loaderUtils = require('loader-utils');
var stylus = require('stylus');
var path = require('path');
var fs = require('fs');
var when = require('when');
var whenNodefn = require('when/node/function');
var cloneDeep = require('lodash.clonedeep');

var CachedPathEvaluator = require('./lib/evaluator');
var PathCache = require('./lib/pathcache');
var resolver = require('./lib/resolver');

var globalImportsCaches = {};
module.exports = function(source) {
  var self = this;
  this.cacheable && this.cacheable();
  var done = this.async();
  var options = cloneDeep(loaderUtils.getOptions(this) || {});
  options.dest = options.dest || '';
  options.filename = options.filename || this.resourcePath;
  options.Evaluator = CachedPathEvaluator;

  var configKey, stylusOptions;
  if (this.stylus) {
    configKey = options.config || 'default';
    stylusOptions = this.stylus[configKey] || {};
  } else if (this.options) {
    configKey = options.config || 'stylus';
    stylusOptions = this.options[configKey] || {};
  } else {
    stylusOptions = {};
  }
  // Instead of assigning to options, we run them manually later so their side effects apply earlier for
  // resolving paths.
  var use = options.use || stylusOptions.use || [];
  options.import = options.import || stylusOptions.import || [];
  options.include = options.include || stylusOptions.include || [];
  options.set = options.set || stylusOptions.set || {};
  options.define = options.define || stylusOptions.define || {};
  options.paths = options.paths || stylusOptions.paths;

  if (options.sourceMap != null) {
    options.sourcemap = options.sourceMap;
    delete options.sourceMap;
  }
  else if (this.sourceMap) {
    options.sourcemap = { comment: false };
  }

  var styl = stylus(source, options);
  var paths = [path.dirname(options.filename)];

  function needsArray(value) {
    return (Array.isArray(value)) ? value : [value];
  }

  if (options.paths && !Array.isArray(options.paths)) {
    paths = paths.concat(options.paths);
    options.paths = [options.paths];
  }

  var manualImports = [];
  Object.keys(options).forEach(function(key) {
    var value = options[key];
    if (key === 'use') {
      needsArray(value).forEach(function(plugin) {
        if (typeof plugin === 'function') {
          styl.use(plugin);
        } else {
          throw new Error('Plugin should be a function');
        }
      });
    } else if (key === 'set') {
      for (var name in value) {
        styl.set(name, value[name]);
      }
    } else if (key === 'define') {
      for (var defineName in value) {
        styl.define(defineName, value[defineName]);
      }
    } else if (key === 'include') {
      needsArray(value).forEach(styl.include.bind(styl));
    } else if (key === 'import') {
      needsArray(value).forEach(function(stylusModule) {
        manualImports.push(stylusModule);
      });
    } else {
      styl.set(key, value);

      if (key === 'resolve url' && value) {
        styl.define('url', resolver());
      }
    }
  });

  var shouldCacheImports = stylusOptions.importsCache !== false;

  var importsCache;
  if (stylusOptions.importsCache !== false) {
    if (typeof stylusOptions.importsCache === 'object') {
      importsCache = stylusOptions.importsCache;
    } else {
      if(!globalImportsCaches[configKey]) globalImportsCaches[configKey] = {};
      importsCache = globalImportsCaches[configKey];
    }
  }

  // Use input file system's readFile if available. The normal webpack input
  // file system is cached with entries purged when they are detected to be
  // changed on disk by the watcher.
  var readFile;
  try {
    var inputFileSystem = this._compiler.inputFileSystem;
    readFile = inputFileSystem.readFile.bind(inputFileSystem);
  } catch (error) {
    readFile = fs.readFile;
  }

  var boundResolvers = PathCache.resolvers(options, this.resolve);
  var pathCacheHelpers = {
    resolvers: boundResolvers,
    readFile: readFile,
  };

  // Use plugins here so that resolve related side effects can be used while we resolve imports.
  (Array.isArray(use) ? use : [use]).forEach(styl.use, styl);

  when
    // Resolve manual imports like @import files.
    .reduce(manualImports, function resolveManualImports(carry, filename) {
      return PathCache.resolvers
        .reduce(boundResolvers, path.dirname(options.filename), filename)
        .then(function(paths) { return carry.concat(paths); });
    }, [])
    // Resolve dependencies of
    .then(function(paths) {
      paths.forEach(styl.import.bind(styl));
      paths.forEach(self.addDependency);

      var readFile = whenNodefn.lift(pathCacheHelpers.readFile);
      return when.reduce(paths, function(cache, filepath) {
        return readFile(filepath)
          .then(function(source) {
            return PathCache.createFromFile(
              pathCacheHelpers, cache, source.toString(), filepath
            );
          });
      }, {
        contexts: {},
        sources: {},
        imports: importsCache,
      });
    })
    .then(function(cache) {
      return PathCache
        .createFromFile(pathCacheHelpers, cache, source, options.filename);
    })
    .then(function(importPathCache) {
      // CachedPathEvaluator will use this PathCache to find its dependencies.
      options.cache = importPathCache;
      importPathCache.allDeps().forEach(function(f) {
        self.addDependency(path.normalize(f));
      });

      // var paths = importPathCache.origins;

      styl.render(function(err, css) {
        if (err) {
          done(err);
        } else {
          if (styl.sourcemap) {
            styl.sourcemap.sourcesContent = styl.sourcemap.sources.map(function (file) {
              return importPathCache.sources[path.resolve(file)]
            });
          }
          done(null, css, styl.sourcemap);
        }
      });
    })
    .catch(done);
};

var LoaderOptionsPlugin = require('webpack').LoaderOptionsPlugin;

// Webpack 2 plugin for setting options that'll be available to stylus-loader.
function OptionsPlugin(options) {
  if (!LoaderOptionsPlugin) {
    throw new Error(
      'webpack.LoaderOptionPlugin is not available. A newer version of webpack is needed.'
    );
  }
  var stylusOptions = {};
  var test = options.test || /\.styl$/;
  var include = options.include;
  var exclude = options.exclude;

  var loaderOptions = {
    stylus: stylusOptions,
  };
  for (var key in options) {
    if (['test', 'include', 'exclude'].indexOf(key) === -1) {
      stylusOptions[key] = options[key];
    }
  }
  if (test) {
    loaderOptions.test = test;
  }
  if (include) {
    loaderOptions.include = include;
  }
  if (exclude) {
    loaderOptions.exclude = exclude;
  }
  this.plugin = new LoaderOptionsPlugin(loaderOptions);
};

module.exports.OptionsPlugin = OptionsPlugin;

OptionsPlugin.prototype.apply = function(compiler) {
  this.plugin.apply(compiler);
};