UNPKG

bem

Version:
498 lines (368 loc) 15.1 kB
'use strict'; var _ = require('underscore'), Q = require('q'), QFS = require('q-fs'), FS = require('fs'), PATH = require('../path'), CP = require('child_process'), VM = require('vm'), LOGGER = require('../logger'), BEM = require('../coa').api, createLevel = require('../level').createLevel, Context = require('../context').Context, U = require('../util'), registry = require('../nodesregistry'), FileNode = require('./file'), GeneratedFileNode = require('./file').GeneratedFileNodeName, BemBuildNodeName = exports.BemBuildNodeName = 'BemBuildNode'; /* jshint -W106 */ exports.__defineGetter__(BemBuildNodeName, function() { return registry.getNodeClass(BemBuildNodeName); }); /* jshint +W106 */ registry.decl(BemBuildNodeName, GeneratedFileNode, { nodeType: 7, __constructor: function(o) { this.bundlesLevel = o.bundlesLevel; this.levelsPaths = o.levels.map(function(level) { return level.path || level; }); this.levels = o.levels.map(function(l) { return createLevel(l, { projectRoot: o.root }); }); this.declPath = o.declPath; this.techName = o.techName; this.output = o.output; this.forked = typeof o.forked === 'undefined'? false : !!o.forked; // NOTE: Every time we need new tech object so we use createTech(). // If we use getTech(), we'll overwrite context of the same tech // object that is prone to collisions this.tech = this.bundlesLevel.getTech(o.techName, o.techPath); this.techPath = this.tech.getTechPath(); this.tech.setContext(new Context(this.bundlesLevel, { level: this.levels })); this.__base(U.extend({ path: this.__self.createId(o) }, o)); }, isValid: function() { if (this.tech.API_VER === 2) return false; var _this = this; return Q.when(this.__base(), function(valid){ if (!valid) return false; var meta = _this.getMetaNode(); if (!_this.ctx.arch.hasNode(meta)) return false; return Q.all([ _this.readMeta(), _this.lastModified(), meta.lastModified() ]) .spread(function(meta, nodeLastModified, metaLastModified) { // expired if <tech>.meta.js is invalid if (meta === null) return false; // expired if .<tech>.meta.js is newer than .<tech> if (metaLastModified > nodeLastModified) return false; // Possible values: promised, callback, sync var strategy = process.env.BEM_IO_STRATEGY_BUILD_IS_VALID || process.env.BEM_IO_STRATEGY; ['promised', 'callback', 'sync'].indexOf(strategy) !== -1 || (strategy = 'callback'); LOGGER.fverbose('Using %s strategy in BemBuildNode.isValid()', strategy); if (strategy === 'promised') { // Promised build result validity check return (function() { var valid = true; return Q.all(meta.map(function(m) { return QFS.lastModified(PATH.resolve(_this.root, m)) .then(function(d) { if (d > nodeLastModified) valid = false; }) .fail(function() { valid = false; }); })) .then(function() { return valid; }); })(); } else if (strategy === 'callback') { // Async build result validity check return (function() { var d = Q.defer(), total = meta.length, count = 0; if (total === 0) d.resolve(true); meta.forEach(function(path) { FS.stat(PATH.resolve(_this.root, path), function(err, stat) { count++; if (err || stat.mtime.getTime() > nodeLastModified) d.resolve(false); if (count < total) return; d.resolve(true); }); }); return d.promise; })(); } // Sync build result validity check // See https://github.com/bem/bem-tools/issues/157 for (var i = 0, l = meta.length; i < l; i++) { if (FS.statSync(PATH.resolve(_this.root, meta[i])).mtime.getTime() > nodeLastModified) return false; } return true; }); }); }, make: function() { var opts = { outputLevel: this.bundlesLevel.dir, level: this.levelsPaths, declaration: PATH.resolve(this.root, this.declPath), tech: this.techPath, outputName: PATH.resolve(this.root, this.output), force: this.ctx.force, root: this.root }; this.log('bem.build(forked=%j, %s)', this.forked, JSON.stringify(opts, null, 4)); if (!this.forked) { opts.level = this.levels; return BEM.build(opts); } opts.forceCache = true; // TODO: generalize forking of bem commands var _this = this, d = Q.defer(), worker = CP.fork(PATH.join(__dirname, 'workers', 'bembuild.js'), null, { env: process.env }), handler = function(m) { (m.code !== 0)? d.reject(m.msg) : d.resolve(); }; /* jshint -W109 */ worker.on('exit', function(code) { LOGGER.fdebug("Exit of bembuild worker for node '%s' with code %s", _this.output, code); handler({ code: code }); }); worker.on('message', function(m) { LOGGER.fdebug("Message from bembuild worker for node '%s': %j", _this.output, m); handler(m); }); /* jshint +W109 */ worker.send(opts); return d.promise; }, readMeta: function() { return this._readMeta(this.getMetaNode().getPath()); }, _readMeta: function(path) { path = PATH.resolve(this.root, path); var _this = this, relativize = getPathRelativizer(this.root); return U.readFile(path) .then(function(c) { return VM.runInThisContext(c).map(function(f) { return relativize(PATH.resolve(PATH.dirname(path), f)); }, _this); }) .fail(function() { return null; }); }, getMetaNode: function() { var node = this.metaNode || (this.metaNode = new exports.BemBuildMetaNode({ root: this.root, bundlesLevel: this.bundlesLevel, levels: this.levelsPaths, declPath: this.declPath, techPath: this.techPath, techName: this.techName, output: this.output, forked: this.forked })); node.buildNode = this; return node; }, /** * Get files minimum mtime in milliseconds or -1 in case of any file doesn't exist. * * @return {Promise * Number} */ lastModified: function() { return Q.all(this.tech .getPaths(PATH.resolve(this.root, this.output), this.tech.getBuildSuffixes()) .map(function(path) { return QFS.lastModified(path) .fail(function() { return -1; }); })) .spread(Math.min); }, /** * clean() implementation. * @return {Promise * Undefined} */ clean: function() { var _this = this; return Q.all(this.tech .getPaths(PATH.resolve(this.root, this.output), this.tech.getBuildSuffixes()) .map(function(path) { return QFS.remove(path) .then(function() { LOGGER.fverbose('[-] Removed %j', path); }) .fail(function() {}); })) .then(function() { return U.removePath(_this.getPath()) .then(function() { LOGGER.fverbose('[-] Removed %j', _this.getId()); }) .fail(function() {}); }); }, getFiles: function() { return this.tech.getPaths(this.output, this.tech.getBuildSuffixes()); }, getDependencies: function() { var deps = this.tech.getDependencies().map(function(d) { return this.bundlesLevel.getPath(this.output, d); }, this); deps.push(this.declPath); return deps; } }, { createId: function(o) { return o.bundlesLevel .getTech(o.techName, o.techPath) .getPath(o.output); } }); var BemBuildMetaNodeName = exports.BemBuildMetaNodeName = 'BemBuildMetaNode'; /* jshint -W106 */ exports.__defineGetter__(BemBuildMetaNodeName, function() { return registry.getNodeClass(BemBuildMetaNodeName); }); /* jshint +W106 */ registry.decl(BemBuildMetaNodeName, BemBuildNodeName, { /** * Overriden. * * Constructs new BemBuildMetaNode instance and changes the path property with `.bem/cache` prefix. * * @param {Object} o */ __constructor: function(o) { this.__base(o); this.path = PATH.join('.bem', 'cache', this.path); }, isValid: function() { var ctx = this.ctx; // expired in case of clean or other methods if (ctx.method && ctx.method !== 'make') return false; var decl = U.readDecl(PATH.resolve(this.root, this.declPath)), tech = this.tech, techv2 = !!this.tech.getBuildPaths, relativize = getPathRelativizer(this.root), prefixes = !techv2? tech.getBuildPrefixes(tech.transformBuildDecl(decl), this.levels): Q.resolve([]), filteredPaths = tech.filterPrefixes(prefixes, tech.getSuffixes()).invoke('map', relativize), savedPaths = this.readMeta(); ctx.prefixes = prefixes; ctx.filteredPaths = filteredPaths; ctx.savedPaths = savedPaths; // expired if build is forced; must return after filling the ctx with promises if (ctx.force) return false; var _this = this; return Q.all([filteredPaths, savedPaths]) .spread(function(filteredPaths, savedPaths) { if (savedPaths === null) return false; var diff = [].concat(_.difference(savedPaths, filteredPaths), _.difference(filteredPaths, savedPaths)); LOGGER.fdebug('*** isValid(%j)=%j', _this.getId(), !diff.length); LOGGER.fdebug('*** diff=%j', diff); LOGGER.fdebug('*** savedPaths=%j', savedPaths); LOGGER.fdebug('*** filteredPaths=%j', filteredPaths); return !diff.length; }); }, make: function() { var _this = this, ctx = this.ctx, arch = ctx.arch; return Q.all([ctx.filteredPaths, ctx.savedPaths]) .spread(function(filteredPaths, savedPaths) { return arch.withLock(_this.alterArch(filteredPaths, savedPaths), _this) .then(function() { // write file list to .meta.js var relativize = getPathRelativizer(PATH.dirname(_this.path)), paths = filteredPaths.map(function(f) { return relativize(f); }); U.mkdirs(PATH.dirname(_this.getPath())); return QFS.write(_this.getPath(), '(' + JSON.stringify(paths, null, 4) + ')'); }); }); }, alterArch: function(filteredPaths, savedPaths) { return function() { var ctx = this.ctx, arch = ctx.arch, buildNodeId = this.buildNode.getId(); // find difference with array read from file and filteredPaths var obsolete = _.difference(savedPaths, filteredPaths); // clean obsolete dependencies from arch obsolete.forEach(function(p) { if (!arch.hasNode(p)) return; // remove link with p arch.unlink(p, buildNodeId); // when p has no other dependent nodes, remove it from arch if (!arch.getParents(p).length) arch.removeNode(p); }); // create nodes for all existent paths to blocks files: FileNode(path) filteredPaths.forEach(function(p) { arch.hasNode(p) || arch.addNode(new FileNode.FileNode({ root: this.root, path: p })); // link created nodes to BemBuildNode corresponding to this node ctx.plan.link(p, buildNodeId); }, this); return Q.when(this.takeSnapshot('after alterArch BemBuildMetaNode ' + this.getId())); }; }, readMeta: function() { return this._readMeta(this.getPath()); }, /** * Get file mtime in milliseconds or -1 in case of file doesn't exist. * * @return {Promise * Number} */ lastModified: function() { // TODO: should really use link to GeneratedFileNode.prototype.lastModified() here return QFS.lastModified(this.getPath()) .fail(function() { return -1; }); }, /** * clean() implementation. * @return {Promise * Undefined} */ clean: function() { // TODO: should really use link to GeneratedFileNode.prototype.clean() here var _this = this; return QFS.remove(this.getPath()) .then(function() { LOGGER.fverbose('[-] Removed %j', _this.getId()); }) .fail(function() {}); }, getDependencies: function() { return [this.declPath]; } }, { createId: function(o) { return this.__base(o) + '.meta.js'; } }); function getPathRelativizer(from) { return function(p) { return PATH.relative(from, p); }; }