index.js 6.73 KB
'use strict';

Object.defineProperty(exports, '__esModule', {
  value: true
});

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; }

var _postcss = require('postcss');

var _postcss2 = _interopRequireDefault(_postcss);

var _cssSelectorTokenizer = require('css-selector-tokenizer');

var _cssSelectorTokenizer2 = _interopRequireDefault(_cssSelectorTokenizer);

var hasOwnProperty = Object.prototype.hasOwnProperty;

function getSingleLocalNamesForComposes(selectors) {
  return selectors.nodes.map(function (node) {
    if (node.type !== 'selector' || node.nodes.length !== 1) {
      throw new Error('composition is only allowed when selector is single :local class name not in "' + _cssSelectorTokenizer2['default'].stringify(selectors) + '"');
    }
    node = node.nodes[0];
    if (node.type !== 'nested-pseudo-class' || node.name !== 'local' || node.nodes.length !== 1) {
      throw new Error('composition is only allowed when selector is single :local class name not in "' + _cssSelectorTokenizer2['default'].stringify(selectors) + '", "' + _cssSelectorTokenizer2['default'].stringify(node) + '" is weird');
    }
    node = node.nodes[0];
    if (node.type !== 'selector' || node.nodes.length !== 1) {
      throw new Error('composition is only allowed when selector is single :local class name not in "' + _cssSelectorTokenizer2['default'].stringify(selectors) + '", "' + _cssSelectorTokenizer2['default'].stringify(node) + '" is weird');
    }
    node = node.nodes[0];
    if (node.type !== 'class') {
      // 'id' is not possible, because you can't compose ids
      throw new Error('composition is only allowed when selector is single :local class name not in "' + _cssSelectorTokenizer2['default'].stringify(selectors) + '", "' + _cssSelectorTokenizer2['default'].stringify(node) + '" is weird');
    }
    return node.name;
  });
}

var processor = _postcss2['default'].plugin('postcss-modules-scope', function (options) {
  return function (css) {
    var generateScopedName = options && options.generateScopedName || processor.generateScopedName;

    var exports = {};

    function exportScopedName(name) {
      var scopedName = generateScopedName(name, css.source.input.from, css.source.input.css);
      exports[name] = exports[name] || [];
      if (exports[name].indexOf(scopedName) < 0) {
        exports[name].push(scopedName);
      }
      return scopedName;
    }

    function localizeNode(node) {
      var newNode = Object.create(node);
      switch (node.type) {
        case 'selector':
          newNode.nodes = node.nodes.map(localizeNode);
          return newNode;
        case 'class':
        case 'id':
          var scopedName = exportScopedName(node.name);
          newNode.name = scopedName;
          return newNode;
      }
      throw new Error(node.type + ' ("' + _cssSelectorTokenizer2['default'].stringify(node) + '") is not allowed in a :local block');
    }

    function traverseNode(node) {
      switch (node.type) {
        case 'nested-pseudo-class':
          if (node.name === 'local') {
            if (node.nodes.length !== 1) {
              throw new Error('Unexpected comma (",") in :local block');
            }
            return localizeNode(node.nodes[0]);
          }
        /* falls through */
        case 'selectors':
        case 'selector':
          var newNode = Object.create(node);
          newNode.nodes = node.nodes.map(traverseNode);
          return newNode;
      }
      return node;
    }

    // Find any :import and remember imported names
    var importedNames = {};
    css.walkRules(function (rule) {
      if (/^:import\(.+\)$/.test(rule.selector)) {
        rule.walkDecls(function (decl) {
          importedNames[decl.prop] = true;
        });
      }
    });

    // Find any :local classes
    css.walkRules(function (rule) {
      var selector = _cssSelectorTokenizer2['default'].parse(rule.selector);
      var newSelector = traverseNode(selector);
      rule.selector = _cssSelectorTokenizer2['default'].stringify(newSelector);
      rule.walkDecls(/composes|compose-with/, function (decl) {
        var localNames = getSingleLocalNamesForComposes(selector);
        var classes = decl.value.split(/\s+/);
        classes.forEach(function (className) {
          var global = /^global\(([^\)]+)\)$/.exec(className);
          if (global) {
            localNames.forEach(function (exportedName) {
              exports[exportedName].push(global[1]);
            });
          } else if (hasOwnProperty.call(importedNames, className)) {
            localNames.forEach(function (exportedName) {
              exports[exportedName].push(className);
            });
          } else if (hasOwnProperty.call(exports, className)) {
            localNames.forEach(function (exportedName) {
              exports[className].forEach(function (item) {
                exports[exportedName].push(item);
              });
            });
          } else {
            throw decl.error('referenced class name "' + className + '" in ' + decl.prop + ' not found');
          }
        });
        decl.remove();
      });

      rule.walkDecls(function (decl) {
        var tokens = decl.value.split(/(,|'[^']*'|"[^"]*")/);
        tokens = tokens.map(function (token, idx) {
          if (idx === 0 || tokens[idx - 1] === ',') {
            var localMatch = /^(\s*):local\s*\((.+?)\)/.exec(token);
            if (localMatch) {
              return localMatch[1] + exportScopedName(localMatch[2]) + token.substr(localMatch[0].length);
            } else {
              return token;
            }
          } else {
            return token;
          }
        });
        decl.value = tokens.join('');
      });
    });

    // Find any :local keyframes
    css.walkAtRules(function (atrule) {
      if (/keyframes$/.test(atrule.name)) {
        var localMatch = /^\s*:local\s*\((.+?)\)\s*$/.exec(atrule.params);
        if (localMatch) {
          atrule.params = exportScopedName(localMatch[1]);
        }
      }
    });

    // If we found any :locals, insert an :export rule
    var exportedNames = Object.keys(exports);
    if (exportedNames.length > 0) {
      (function () {
        var exportRule = _postcss2['default'].rule({ selector: ':export' });
        exportedNames.forEach(function (exportedName) {
          return exportRule.append({
            prop: exportedName,
            value: exports[exportedName].join(' '),
            raws: { before: '\n  ' }
          });
        });
        css.append(exportRule);
      })();
    }
  };
});

processor.generateScopedName = function (exportedName, path) {
  var sanitisedPath = path.replace(/\.[^\.\/\\]+$/, '').replace(/[\W_]+/g, '_').replace(/^_|_$/g, '');
  return '_' + sanitisedPath + '__' + exportedName;
};

exports['default'] = processor;
module.exports = exports['default'];