UNPKG

gulp-order-bemdeps

Version:

Reorder a stream of files using deps.js files contents

341 lines (262 loc) 10.8 kB
'use strict'; 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; }