index.js 5.59 KB
Newer Older
liang ce committed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181
'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 postcss = require('postcss');
var topologicalSort = require('./topologicalSort');

var declWhitelist = ['composes'];
var declFilter = new RegExp(`^(${declWhitelist.join('|')})$`);
var matchImports = /^(.+?)\s+from\s+(?:"([^"]+)"|'([^']+)'|(global))$/;
var icssImport = /^:import\((?:"([^"]+)"|'([^']+)')\)/;

var VISITED_MARKER = 1;

function createParentName(rule, root) {
  return `__${root.index(rule.parent)}_${rule.selector}`;
}

function serializeImports(imports) {
  return imports.map(function (importPath) {
    return '`' + importPath + '`';
  }).join(', ');
}

/**
 * :import('G') {}
 *
 * Rule
 *   composes: ... from 'A'
 *   composes: ... from 'B'

 * Rule
 *   composes: ... from 'A'
 *   composes: ... from 'A'
 *   composes: ... from 'C'
 *
 * Results in:
 *
 * graph: {
 *   G: [],
 *   A: [],
 *   B: ['A'],
 *   C: ['A'],
 * }
 */
function addImportToGraph(importId, parentId, graph, visited) {
  var siblingsId = parentId + '_' + 'siblings';
  var visitedId = parentId + '_' + importId;

  if (visited[visitedId] !== VISITED_MARKER) {
    if (!Array.isArray(visited[siblingsId])) visited[siblingsId] = [];

    var siblings = visited[siblingsId];

    if (Array.isArray(graph[importId])) graph[importId] = graph[importId].concat(siblings);else graph[importId] = siblings.slice();

    visited[visitedId] = VISITED_MARKER;
    siblings.push(importId);
  }
}

module.exports = postcss.plugin('modules-extract-imports', function () {
  var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};

  var failOnWrongOrder = options.failOnWrongOrder;

  return function (css) {
    var graph = {};
    var visited = {};

    var existingImports = {};
    var importDecls = {};
    var imports = {};

    var importIndex = 0;

    var createImportedName = typeof options.createImportedName !== 'function' ? function (importName /*, path*/) {
      return `i__imported_${importName.replace(/\W/g, '_')}_${importIndex++}`;
    } : options.createImportedName;

    // Check the existing imports order and save refs
    css.walkRules(function (rule) {
      var matches = icssImport.exec(rule.selector);

      if (matches) {
        var _matches = _slicedToArray(matches, 3),
            /*match*/doubleQuotePath = _matches[1],
            singleQuotePath = _matches[2];

        var importPath = doubleQuotePath || singleQuotePath;

        addImportToGraph(importPath, 'root', graph, visited);

        existingImports[importPath] = rule;
      }
    });

    // Find any declaration that supports imports
    css.walkDecls(declFilter, function (decl) {
      var matches = decl.value.match(matchImports);
      var tmpSymbols = void 0;

      if (matches) {
        var _matches2 = _slicedToArray(matches, 5),

        /*match*/symbols = _matches2[1],
            doubleQuotePath = _matches2[2],
            singleQuotePath = _matches2[3],
            global = _matches2[4];

        if (global) {
          // Composing globals simply means changing these classes to wrap them in global(name)
          tmpSymbols = symbols.split(/\s+/).map(function (s) {
            return `global(${s})`;
          });
        } else {
          var importPath = doubleQuotePath || singleQuotePath;
          var parentRule = createParentName(decl.parent, css);

          addImportToGraph(importPath, parentRule, graph, visited);

          importDecls[importPath] = decl;
          imports[importPath] = imports[importPath] || {};

          tmpSymbols = symbols.split(/\s+/).map(function (s) {
            if (!imports[importPath][s]) {
              imports[importPath][s] = createImportedName(s, importPath);
            }

            return imports[importPath][s];
          });
        }

        decl.value = tmpSymbols.join(' ');
      }
    });

    var importsOrder = topologicalSort(graph, failOnWrongOrder);

    if (importsOrder instanceof Error) {
      var importPath = importsOrder.nodes.find(function (importPath) {
        return importDecls.hasOwnProperty(importPath);
      });
      var decl = importDecls[importPath];

      var errMsg = 'Failed to resolve order of composed modules ' + serializeImports(importsOrder.nodes) + '.';

      throw decl.error(errMsg, {
        plugin: 'modules-extract-imports',
        word: 'composes'
      });
    }

    var lastImportRule = void 0;
    importsOrder.forEach(function (path) {
      var importedSymbols = imports[path];
      var rule = existingImports[path];

      if (!rule && importedSymbols) {
        rule = postcss.rule({
          selector: `:import("${path}")`,
          raws: { after: '\n' }
        });

        if (lastImportRule) css.insertAfter(lastImportRule, rule);else css.prepend(rule);
      }

      lastImportRule = rule;

      if (!importedSymbols) return;

      Object.keys(importedSymbols).forEach(function (importedSymbol) {
        rule.append(postcss.decl({
          value: importedSymbol,
          prop: importedSymbols[importedSymbol],
          raws: { before: '\n  ' }
        }));
      });
    });
  };
});