UNPKG

enb

Version:

Faster BEM/BEViS assembler

609 lines (558 loc) 21.9 kB
var Vow = require('vow'); var Node = require('./node'); var path = require('path'); var Logger = require('./logger'); var colors = require('./ui/colorize'); var ProjectConfig = require('./config/project-config'); var Cache = require('./cache/cache'); var CacheStorage = require('./cache/cache-storage'); var inherit = require('inherit'); var vowFs = require('./fs/async-fs'); var fs = require('fs'); var BuildGraph = require('./ui/build-graph'); var TargetNotFoundError = require('./errors/target-not-found-error'); var dropRequireCache = require('./fs/drop-require-cache'); /** * MakePlatform * ============ * * Класс MakePlatform управляет сборкой проекта. * В процессе инициализации загружается {CWD}/.bem/enb-make.js, в котором содержатся правила сборки. * @name MakePlatform * @class */ module.exports = inherit( /** @lends MakePlatform.prototype */ { /** * Конструктор. */ __constructor: function () { this._nodes = {}; this._nodeInitPromises = {}; this._cacheStorage = null; this._cache = null; this._projectConfig = null; this._cdir = null; this._languages = null; this._env = {}; this._mode = null; this._makefiles = []; this._graph = null; this._levelNamingSchemes = {}; }, /** * Инициализация make-платформы. * Создает директорию для хранения временных файлов, загружает конфиг для сборки. * @param {String} cdir Путь к директории с проектом. * @param {String} [mode] Режим сборки. Например, development. * @param {String} [config] Конфиг сборки. По умолчанию загружается из `.enb/make.js`. * @returns {Promise} */ init: function (cdir, mode, config) { this._mode = mode = mode || process.env.YENV || 'development'; this._cdir = cdir; var _this = this; var projectName = path.basename(cdir); var configDir = this._getConfigDir(); var projectConfig = this._projectConfig = new ProjectConfig(cdir); this._projectName = projectName; this._logger = new Logger(); this._buildState = {}; this._graph = new BuildGraph(projectName); try { if (config) { config(projectConfig); } else { var makefilePath = this._getMakeFile('make'); var personalMakefilePath = this._getMakeFile('make.personal'); if (!makefilePath) { throw new Error('Cannot find make configuration file.'); } this._makefiles = [makefilePath, personalMakefilePath]; dropRequireCache(require, makefilePath); require(makefilePath)(projectConfig); if (personalMakefilePath) { dropRequireCache(require, personalMakefilePath); require(personalMakefilePath)(projectConfig); } } } catch (err) { return Vow.reject(err); } this._makefiles = this._makefiles.concat(projectConfig.getIncludedConfigFilenames()); var modeConfig = projectConfig.getModeConfig(mode); if (modeConfig) { modeConfig.exec(null, projectConfig); } this._languages = projectConfig.getLanguages(); this._env = projectConfig.getEnvValues(); this._levelNamingSchemes = projectConfig.getLevelNamingSchemes(); projectConfig.task('clean', function (task) { return task.cleanTargets([].slice.call(arguments, 1)); }); var tmpDir = configDir + '/tmp'; return vowFs.makeDir(tmpDir).then(function () { _this._cacheStorage = new CacheStorage(tmpDir + '/cache.js'); _this._nodes = {}; }); }, /** * Возвращает абсолютный путь к директории с проектом. * @returns {String} */ getDir: function () { return this._cdir; }, /** * Возвращает абсолютный путь к директории с конфигурационными файлами. * В качестве директории ожидается либо .enb/, либо .bem/. * @returns {string} * @private */ _getConfigDir: function () { var cdir = this.getDir(); var possibleDirs = ['.enb', '.bem']; var configDir; var isConfigDirExists = possibleDirs.some(function (dir) { configDir = path.join(cdir, dir); return fs.existsSync(configDir); }); if (isConfigDirExists) { return configDir; } else { throw new Error('Cannot find enb config directory. Should be either .enb/ or .bem/.'); } }, /** * Возвращает путь к указанному конфигу сборки. * Если файлов make.js и make.personal.js не существует, то пробуем искать файлы с префиксом enb-. * @param {String} file Название конфига (основной или персональный). * @returns {String} * @private */ _getMakeFile: function (file) { var configDir = this._getConfigDir(); var possiblePrefixes = ['enb-', '']; var makeFile; var isMakeFileExists = possiblePrefixes.some(function (prefix) { makeFile = path.join(configDir, prefix + file + '.js'); return fs.existsSync(makeFile); }); if (isMakeFileExists) { return makeFile; } }, /** * Возвращает построитель графа сборки. * @returns {BuildGraph} */ getBuildGraph: function () { return this._graph; }, /** * Загружает кэш из временной папки. * В случае, если обновился пакет enb, либо изменился режим сборки, либо изменились make-файлы, сбрасывается кэш. */ loadCache: function () { this._cacheStorage.load(); var version = require('../package.json').version; var mtimes = this._cacheStorage.get(':make', 'makefiles') || {}; var dropCache = false; // Invalidate cache if mode was changed. if (this._cacheStorage.get(':make', 'mode') !== this._mode) { dropCache = true; } // Invalidate cache if ENB package was updated. if (this._cacheStorage.get(':make', 'version') !== version) { dropCache = true; } // Invalidate cache if any of makefiles were updated. var currentMTimes = this._getMakefileMTimes(); Object.keys(currentMTimes).forEach(function (makefilePath) { if (currentMTimes[makefilePath] !== mtimes[makefilePath]) { dropCache = true; } }); if (dropCache) { this._cacheStorage.drop(); } }, /** * Возвращает время изменения каждого загруженного make-файла в виде unix-time (.bem/enb-make.js). * @returns {Object} * @private */ _getMakefileMTimes: function () { var res = {}; this._makefiles.forEach(function (makefilePath) { if (fs.existsSync(makefilePath)) { res[makefilePath] = fs.statSync(makefilePath).mtime.getTime(); } }); return res; }, /** * Сохраняет кэш во временную папку. */ saveCache: function () { this._setCacheAttrs(); return this._cacheStorage.save(); }, /** * Сохраняет кэш во временную папку асинхронно. */ saveCacheAsync: function () { this._setCacheAttrs(); return this._cacheStorage.saveAsync(); }, _setCacheAttrs: function () { this._cacheStorage.set(':make', 'mode', this._mode); this._cacheStorage.set(':make', 'version', require('../package.json').version); this._cacheStorage.set(':make', 'makefiles', this._getMakefileMTimes()); }, /** * Возвращает переменные окружения. * @returns {Object} */ getEnv: function () { return this._env; }, /** * Устанавливает переменные окружения. * @param {Object} env */ setEnv: function (env) { this._env = env; }, /** * Возвращает хранилище кэша. * @returns {CacheStorage} */ getCacheStorage: function () { return this._cacheStorage; }, /** * Устанавливает хранилище кэша. * @param {CacheStorage} cacheStorage */ setCacheStorage: function (cacheStorage) { this._cacheStorage = cacheStorage; }, /** * Возвращает языки для проекта. * Вроде, уже больше не нужно. Надо избавиться в будущих версиях. * @returns {String[]} * @deprecated */ getLanguages: function () { return this._languages; }, /** * Устанавливает языки для проекта. * Вроде, уже больше не нужно. Надо избавиться в будущих версиях. * @param {String[]} languages * @deprecated */ setLanguages: function (languages) { this._languages = languages; }, /** * Возвращает логгер для сборки. * @returns {Logger} */ getLogger: function () { return this._logger; }, /** * Устанавливает логгер для сборки. * Позволяет перенаправить вывод процесса сборки. * * @param {Logger} logger */ setLogger: function (logger) { this._logger = logger; }, /** * Инициализирует ноду по нужному пути. * @param {String} nodePath * @returns {Promise} */ initNode: function (nodePath) { if (!this._nodeInitPromises[nodePath]) { var _this = this; var cdir = this.getDir(); var nodeConfig = this._projectConfig.getNodeConfig(nodePath); var node = new Node(nodePath, this, this._cache); node.setLogger(this._logger.subLogger(nodePath)); node.setBuildGraph(this._graph); this._nodes[nodePath] = node; this._nodeInitPromises[nodePath] = vowFs.makeDir(path.join(cdir, nodePath)) .then(function () { return Vow.when(nodeConfig.exec()); }) .then(function () { return Vow.all(_this._projectConfig.getNodeMaskConfigs(nodePath).map(function (nodeMaskConfig) { return nodeMaskConfig.exec([], nodeConfig); })); }) .then(function () { var mode = nodeConfig.getModeConfig(_this._mode); return mode && mode.exec(null, nodeConfig); }) .then(function () { node.setLanguages(nodeConfig.getLanguages() || _this._languages); node.setTargetsToBuild(nodeConfig.getTargets()); node.setTargetsToClean(nodeConfig.getCleanTargets()); node.setTechs(nodeConfig.getTechs()); node.setBuildState(_this._buildState); return node.loadTechs(); }); } return this._nodeInitPromises[nodePath]; }, /** * Требует сборки таргетов для указанной ноды. * @param {String} nodePath Например, "pages/index". * @param {String[]} sources Таргеты, которые необходимо собрать. * @returns {Promise} */ requireNodeSources: function (nodePath, sources) { var _this = this; return this.initNode(nodePath).then(function () { return _this._nodes[nodePath].requireSources(sources); }); }, /** * Сбрасывает кэш. */ dropCache: function () { this._cacheStorage.drop(); }, /** * Возвращает массив строк путей к нодам, упорядоченные по убыванию длины. * Сортировка по убыванию нужна для случаев, когда на файловой системе одна нода находится * внутри другой (например, `bundles/page` и `bundles/page/bundles/header`). * * @returns {String[]} * @private */ _getNodePathsLenDesc: function () { return Object.keys(this._projectConfig.getNodeConfigs()).sort(function (a, b) { return b.length - a.length; }); }, /** * Вычисляет (на основе переданного пути к таргету и списка путей к нодам) * к какой ноде принадлежит переданный таргет. * @param {String} target * @param {String[]} nodePaths * @returns {{node: *, targets: String[]}} * @private */ _resolveTarget: function (target, nodePaths) { target = target.replace(/^(\.\/)+|\/$/g, ''); for (var i = 0, l = nodePaths.length; i < l; i++) { var nodePath = nodePaths[i]; if (target.indexOf(nodePath) === 0) { var npl = nodePath.length; var charAtNpl = target.charAt(npl); if (target.length === npl) { return { node: nodePath, targets: ['*'] }; } else if (charAtNpl === '/' || charAtNpl === '\\') { return { node: nodePath, targets: [target.substr(npl + 1)] }; } } } throw TargetNotFoundError('Target not found: ' + target); }, /** * Вычисляет для списка таргетов, к каким нодам они принадлежат. * @param {String[]} targets * @returns {Object[]} * @private */ _resolveTargets: function (targets) { var _this = this; var buildTargets = []; var nodeConfigs = this._projectConfig.getNodeConfigs(); var nodePathsDesc = this._getNodePathsLenDesc(); if (targets.length) { var targetIndex = {}; targets.forEach(function (targetName) { var target = _this._resolveTarget(targetName, nodePathsDesc); if (targetIndex[target.node]) { var currentTargetList = targetIndex[target.node].targets; target.targets.forEach(function (resTargetName) { if (currentTargetList.indexOf(resTargetName) === -1) { currentTargetList.push(resTargetName); } }); } else { targetIndex[target.node] = target; buildTargets.push(target); } }); } else { Object.keys(nodeConfigs).forEach(function (nodePath) { buildTargets.push({ node: nodePath, targets: ['*'] }); }); } return buildTargets; }, /** * Запускает сборку переданного списка таргетов. * @param {String[]} targets * @returns {Promise} */ buildTargets: function (targets) { var _this = this; this._cache = new Cache(this._cacheStorage, this._projectName); try { var targetList = this._resolveTargets(targets); return Vow.all(targetList.map(function (target) { return _this.initNode(target.node); })).then(function () { return Vow.all(targetList.map(function (target) { return _this._nodes[target.node].build(target.targets); })).then(function (builtInfoList) { var builtTargets = []; builtInfoList.forEach(function (builtInfo) { builtTargets = builtTargets.concat(builtInfo.builtTargets); }); return { builtTargets: builtTargets }; }); }); } catch (err) { return Vow.reject(err); } }, /** * @returns {ProjectConfig} */ getProjectConfig: function () { return this._projectConfig; }, /** * Запускает удаление переданного списка таргетов. * @param {String[]} targets * @returns {Promise} */ cleanTargets: function (targets) { var _this = this; this._cache = new Cache(this._cacheStorage, this._projectName); try { var targetList = this._resolveTargets(targets); return Vow.all(targetList.map(function (target) { return _this.initNode(target.node); })).then(function () { return Vow.all(targetList.map(function (target) { return _this._nodes[target.node].clean(target.targets); })); }); } catch (err) { return Vow.reject(err); } }, /** * Запускает выполнение таска. * @param {String} taskName * @param {String[]} args * @returns {Promise} */ buildTask: function (taskName, args) { var task = this._projectConfig.getTaskConfig(taskName); task.setMakePlatform(this); return Vow.when(task.exec(args)); }, /** * Деструктор. */ destruct: function () { this._buildState = null; delete this._projectConfig; var nodes = this._nodes; Object.keys(nodes).forEach(function (nodeName) { nodes[nodeName].destruct(); }); delete this._nodes; if (this._cacheStorage) { this._cacheStorage.drop(); delete this._cacheStorage; } if (this._cache) { this._cache.destruct(); delete this._cache; } delete this._levelNamingSchemes; }, /** * Заменяет слэши в путях к таргетам на обратные, если используется ОС Windows * @param {Array} targets * @returns {Array} */ _fixPath: function (targets) { return path.sep === '/' ? targets : targets.map(function (target) { return target.replace(/\//g, '\\'); }); }, /** * Запускает сборку. * Может запустить либо сборку таргетов, либо запуск тасков. * @param {String[]} targets * @returns {Promise} */ build: function (targets) { targets = this._fixPath(targets); var promise = Vow.promise(); var startTime = new Date(); var _this = this; var targetTask; try { this._logger.log('build started'); if (targets.length && this._projectConfig.getTaskConfig(targets[0])) { targetTask = this.buildTask(targets[0], targets.slice(1)); } else { targetTask = this.buildTargets(targets); } targetTask.then(function () { _this._logger.log('build finished - ' + colors.red((new Date() - startTime) + 'ms')); Object.keys(_this._nodes).forEach(function (nodeName) { _this._nodes[nodeName].getLogger().setEnabled(false); }); promise.fulfill(); }, function (err) { _this._logger.log('build failed'); promise.reject(err); }); } catch (err) { promise.reject(err); } return promise; }, /** * Возвращает схему именования для уровня переопределения. * Схема именования содержит два метода: * ```javascript * // Выполняет построение структуры файлов уровня переопределения, используя методы инстанции класса LevelBuilder. * {Promise} buildLevel( {String} levelPath, {LevelBuilder} levelBuilder ) * // Возвращает путь к файлу на основе пути к уровню переопределения и BEM-описания. * {String} buildFilePath( * {String} levelPath, {String} blockName, {String} elemName, {String} modName, {String} modVal * ) * ``` * @returns {Object|undefined} */ getLevelNamingScheme: function (levelPath) { return this._levelNamingSchemes[levelPath]; } });