gulp-order-bemdeps
Version:
Reorder a stream of files using deps.js files contents
341 lines (262 loc) • 10.8 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
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"); } }; }();
exports.default = gulpOrderBemDeps;
var _vm = require('vm');
var _vm2 = _interopRequireDefault(_vm);
var _ansiColors = require('ansi-colors');
var _ansiColors2 = _interopRequireDefault(_ansiColors);
var _pluginError = require('plugin-error');
var _pluginError2 = _interopRequireDefault(_pluginError);
var _parseBemIdentifier = require('parse-bem-identifier');
var _parseBemIdentifier2 = _interopRequireDefault(_parseBemIdentifier);
var _through = require('through2');
var _through2 = _interopRequireDefault(_through);
var _topologicalSort = require('topological-sort');
var _collectStreamFiles = require('./lib/collect-stream-files');
var _collectStreamFiles2 = _interopRequireDefault(_collectStreamFiles);
var _bemNamingToClassname = require('./lib/bem-naming-to-classname');
var _bemNamingToClassname2 = _interopRequireDefault(_bemNamingToClassname);
var _getFileStem = require('./lib/get-file-stem');
var _getFileStem2 = _interopRequireDefault(_getFileStem);
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
const PLUGIN_NAME = 'gulp-order-bemdeps';
const BEM_NAMING_PARSED_KEYS = ['block', 'mod', 'modVal', 'elem', 'elemMod', 'elemVal'];
/**
* Helper function
* Detects whether stem is block-only or not
*
* @param {Object} bemNaming - object got from bemNamingParser
* @return {Boolean}
*/
function isBlockBemNaming(bemNaming) {
return BEM_NAMING_PARSED_KEYS.every(key => {
return key === 'block' ? bemNaming[key] !== undefined : bemNaming[key] === undefined;
});
}
/**
* Helper function
* Returns stream to be exported out of main exported function
* And promise which becomes resolved when all of input files are collected
*
* @return {Object}
*/
function getStreamAndPromiseForInputStream() {
const files = [];
let resolver;
const promise = new Promise(resolve => {
resolver = resolve;
});
const stream = _through2.default.obj((file, encoding, callback) => {
files.push(file);
callback();
}, function (closeStreamCallback) {
resolver({
files,
closeStreamCallback,
ctx: this
});
});
return { stream, promise };
}
/**
* Flatten dependencies from deps.js files
*
* @return {Array<String>}
* @see https://en.bem.info/technology/deps/about/#depsjs-syntax
*/
function flattenDepsJS(deps) {
let output = [];
if (!Array.isArray(deps)) {
deps = [deps];
}
deps.forEach(dependency => {
if (dependency.tech || dependency.noDeps) {
return;
}
if (dependency.mustDeps) {
output = output.concat(flattenDepsJS(dependency.mustDeps));
return;
}
let dependencyStem = dependency.block;
if (typeof dependency.mods === 'object') {
if (Array.isArray(dependency.mods)) {
for (let modName of dependency.mods) {
output.push(`${dependencyStem}_${modName}`);
}
} else {
Object.keys(dependency.mods).forEach(modName => {
const modVal = dependency.mods[modName];
if (Array.isArray(modVal)) {
for (let modFinalVal of modVal) {
output.push(`${dependencyStem}_${modName}_${modFinalVal}`);
}
} else if (typeof modVal === 'boolean') {
output.push(`${dependencyStem}_${modName}`);
} else {
output.push(`${dependencyStem}_${modName}_${modVal}`);
output.push(`${dependencyStem}_${modName}`);
}
});
}
} else if (Array.isArray(dependency.elems)) {
for (let elem of dependency.elems) {
output.push(`${dependencyStem}__${elem}`);
}
} else {
if (dependency.mod) {
dependencyStem += `_${dependency.mod}`;
}
if (dependency.val) {
dependencyStem += `_${dependency.val}`;
}
if (dependency.elem) {
dependencyStem += `__${dependency.elem}`;
let elemMods = dependency.elemMods;
if (elemMods) {
Object.keys(elemMods).forEach(modName => {
let modVal = elemMods[modName];
dependencyStem += `_${modName}`;
output.push(dependencyStem);
if (typeof modVal !== 'boolean') {
dependencyStem += `_${modVal}`;
}
});
}
}
output.push(dependencyStem);
}
});
return output;
}
/**
* Parse deps.js file contents into flat set of dependencies
*
* @param {String} contents
* @return {Set<String>}
*/
function parseDependencies(contents) {
let deps = _vm2.default.runInThisContext(contents);
return new Set(flattenDepsJS(deps).filter(Boolean));
}
/**
* Build block/element basic dependencies from file name
* What is basic dependency?
* If file stem is `block__elem` then it is Set(`block`)
* If file stem is `block_mod_val__elem` then it is Set(`block`, `block_mod_val`)
*
* @param {String} stem
* @return {Array<String>}
*/
function buildBasicDependencies(stem) {
const bemNaming = (0, _parseBemIdentifier2.default)(stem);
const output = [];
// validate bem naming
const isBadNaming = BEM_NAMING_PARSED_KEYS.some(key => bemNaming[key] === '');
if (isBadNaming) {
throw new _pluginError2.default(PLUGIN_NAME, `Invalid bem naming used: ${stem}`, { showStack: true });
}
if (isBlockBemNaming(bemNaming)) {
return output;
}
if (bemNaming.elemModVal) {
delete bemNaming.elemModVal;
} else if (bemNaming.elemMod) {
delete bemNaming.elemMod;
} else if (bemNaming.elem) {
delete bemNaming.elem;
} else if (bemNaming.modVal) {
delete bemNaming.modVal;
} else if (bemNaming.mod) {
delete bemNaming.mod;
}
const dependencyStem = (0, _bemNamingToClassname2.default)(bemNaming);
output.push(dependencyStem);
return output.concat(buildBasicDependencies(dependencyStem));
}
/**
* BEM files reorder
* It takes deps.js files stream as an argument and reorders input files based on built dependency tree
* Uses topological sort for building dependency tree.
*
* @param {Stream} deps - stream of vinyl deps files (use gulp.src() for this)
* @return {Stream}
*/
function gulpOrderBemDeps(deps) {
let streamCtx;
// wait for all input files
var _getStreamAndPromiseF = getStreamAndPromiseForInputStream();
const output = _getStreamAndPromiseF.stream,
inputPromise = _getStreamAndPromiseF.promise;
Promise.all([(0, _collectStreamFiles2.default)(deps), inputPromise]).then((_ref) => {
var _ref2 = _slicedToArray(_ref, 2);
let depsFiles = _ref2[0];
var _ref2$ = _ref2[1];
let files = _ref2$.files,
ctx = _ref2$.ctx,
closeStreamCallback = _ref2$.closeStreamCallback;
streamCtx = ctx;
// we need to merge files (input stream) with depsFiles (function argument stream)
// output should also contain basic dependencies (`block` for `block__elem`)
// we also need to distinguish between existing files in the merged set
// and those which don't exist (there's no such file in `files` array)
const mergedNodes = new Map();
const edges = [];
// first add all dependencies with their dependencies
for (let dependencyFile of depsFiles) {
const stem = (0, _getFileStem2.default)(dependencyFile.path, '.deps.js');
const stemDependencies = parseDependencies(dependencyFile.contents.toString('utf8'));
if (!mergedNodes.has(stem)) {
mergedNodes.set(stem, {});
}
for (let dependencyStem of stemDependencies) {
if (!mergedNodes.has(dependencyStem)) {
mergedNodes.set(dependencyStem, {});
}
edges.push([dependencyStem, stem]);
}
}
// then add all files and their basic dependencies
for (let inputFile of files) {
const stem = (0, _getFileStem2.default)(inputFile.path);
const mergedNode = mergedNodes.get(stem) || {};
mergedNode.file = inputFile;
mergedNodes.set(stem, mergedNode);
const fileDependencies = buildBasicDependencies(stem);
for (let i = 0; i < fileDependencies.length; i++) {
const dependencyFileStem = fileDependencies[i];
if (!mergedNodes.has(dependencyFileStem)) {
mergedNodes.set(dependencyFileStem, {});
}
const toEdgeKey = i === 0 ? stem : fileDependencies[i - 1];
edges.push([fileDependencies[i], toEdgeKey]);
}
}
const sortOp = new _topologicalSort.TopologicalSort(mergedNodes);
for (let _ref3 of edges) {
var _ref4 = _slicedToArray(_ref3, 2);
let fromKey = _ref4[0];
let toKey = _ref4[1];
sortOp.addEdge(fromKey, toKey);
}
const sorted = sortOp.sort();
for (let _ref5 of sorted) {
var _ref6 = _slicedToArray(_ref5, 2);
let value = _ref6[1];
if (value.file) {
ctx.push(value.file);
}
}
// close stream
closeStreamCallback();
}).catch(err => {
/* eslint-disable no-console */
console.error(_ansiColors2.default.red(err.message));
console.error(err.toString());
/* eslint-enable no-console */
streamCtx.emit('error', err);
});
return output;
}