diff --git a/lib/create-entity.js b/lib/create-entity.js index b813721..904bd85 100644 --- a/lib/create-entity.js +++ b/lib/create-entity.js @@ -3,21 +3,18 @@ var fs = require('fs'), path = require('path'), stream = require('stream'), - bemNaming = require('@bem/naming'), Promise = require('pinkie-promise'), mkdirp = require('mkdirp'), createTree = require('./create-tree'), relativePath = function(to) { return path.relative(process.cwd(), to); }; -module.exports = function(entity, fileName, template, options) { +module.exports = function(fileName, content, options) { return new Promise(function(resolve, reject) { function onEnd(error) { error ? reject(error) : resolve(fileName); } - var isPiped = template instanceof stream.Readable, - content = (isPiped || typeof template === 'string') ? template : - template(entity, bemNaming(options.naming)), + var isPiped = content instanceof stream.Readable, isFile = isPiped || (typeof content !== 'object'); fs.exists(fileName, function(exists) { diff --git a/lib/create.js b/lib/create.js index ad4905a..75269ca 100644 --- a/lib/create.js +++ b/lib/create.js @@ -1,110 +1,16 @@ -'use strict'; - -var path = require('path'), - bemConfig = require('bem-config'), - BemEntityName = require('@bem/entity-name'), - BemCell = require('@bem/cell'), - scheme = require('@bem/fs-scheme'), - bemNaming = require('@bem/naming'), - braceExpansion = require('brace-expansion'), - createEntity = require('./create-entity'), - getTemplate = require('./template'), - uniq = require('uniq'), - Promise = require('pinkie-promise'); +const getEntityData = require('./get-entity-data'); +const getPath = require('./get-path'); +const template = require('./template'); +const createEntity = require('./create-entity'); module.exports = function create(entities, levels, techs, options) { - options || (options = {}); - techs || (techs = []); - var baseConfig = bemConfig(options), - cwd = path.resolve(options.cwd || ''); - - return baseConfig.module('bem-tools') - .then(function(bemToolsConf) { - var pluginConf = bemToolsConf && bemToolsConf.plugins && bemToolsConf.plugins.create || {}; - - return bemConfig(Object.assign({}, options, { extendBy: pluginConf })); - }).then(function(config) { - if (!levels || !levels.length) { - var levelsMap = config.levelMapSync(), - levelList = Object.keys(levelsMap); - - var defaultLevels = levelList.filter(function(level) { - return levelsMap[level].default; - }); - - var levelByCwd = levelList.filter(function(level) { - return cwd.indexOf(level) === 0; - }).sort().reverse()[0]; - - levels = levelByCwd || (defaultLevels.length ? defaultLevels : cwd); - } - - Array.isArray(entities) || (entities = [entities]); - Array.isArray(levels) || (levels = [levels]); - - return Promise.all(entities.map(function(input) { - var isFileGlob = typeof input === 'string'; - - return Promise.all((isFileGlob ? braceExpansion(input) : [input]).map(function(filepathOrInput) { - var currentLevels = levels; - - if (typeof filepathOrInput === 'string') { - var currentLevel = path.dirname(filepathOrInput); - currentLevel !== '.' && (currentLevels = [currentLevel]); - } - - return Promise.all(currentLevels.map(function(relLevel) { - var rootDir = config.rootSync() || cwd, - level = path.resolve(rootDir, relLevel); - - return config.level(level).then(function(levelOptions) { - levelOptions || (levelOptions = {}); - - var levelScheme = levelOptions.scheme, - buildPath = scheme(levelScheme).path, - currentTechs = uniq([].concat(levelOptions.techs || [], techs)), - entity; - - if (isFileGlob) { - var file = path.basename(filepathOrInput), - // split for entity key and tech (by first dot) - match = file.match(/^([^.]+)(?:\.(.+))?$/); - - entity = bemNaming(levelOptions.naming).parse(match[1]); - if (match[2]) { - currentTechs = uniq(techs.concat(match[2])); - } - } else { - entity = BemEntityName.create(filepathOrInput); - } - - options.onlyTech && (currentTechs = options.onlyTech); - - options.excludeTech && (currentTechs = currentTechs.filter(function(tech) { - return options.excludeTech.indexOf(tech) === -1; - })); - - return Promise.all(currentTechs.map(function(tech) { - var pathToFile = buildPath( - new BemCell({ entity: entity, tech: tech }), - levelOptions.schemeOptions || levelOptions), - absPathToFile = path.join(path.resolve(level), pathToFile), - template = options.fileContent || getTemplate(tech, levelOptions); - - levelOptions.forceRewrite = options.forceRewrite; - - return createEntity(entity, absPathToFile, template, levelOptions); - })); - }); - })); - - })); - })).then(flatten); - }); + var entityData = getEntityData(entities, levels, techs, options); + + return Promise.all(entityData.map(item => + template.apply(item) + .then(content => + createEntity(getPath(item), content, item.levelOptions) + ) + ) + ); }; - -function flatten(arr) { - return arr.reduce(function(acc, item) { - return acc.concat(Array.isArray(item) ? flatten(item) : item); - }, []); -} diff --git a/lib/get-entity-data.js b/lib/get-entity-data.js new file mode 100644 index 0000000..73e4aa5 --- /dev/null +++ b/lib/get-entity-data.js @@ -0,0 +1,100 @@ +'use strict'; + +var path = require('path'), + bemNaming = require('@bem/naming'), + BemEntityName = require('@bem/entity-name'), + braceExpansion = require('brace-expansion'), + uniq = require('uniq'), + bemConfig = require('bem-config'); + +function flatten(arr) { + return arr.reduce(function(acc, item) { + return acc.concat(Array.isArray(item) ? flatten(item) : item); + }, []); +} + +module.exports = function getEntityData(entities, levels, techs, options) { + options || (options = {}); + techs || (techs = []); + var baseConfig = bemConfig(options), + cwd = path.resolve(options.cwd || ''), + + bemToolsConf = baseConfig.moduleSync('bem-tools'), + + pluginConf = bemToolsConf && bemToolsConf.plugins && bemToolsConf.plugins.create || {}, + + config = bemConfig(Object.assign({}, options, { extendBy: pluginConf })); + + if (!levels || !levels.length) { + var levelsMap = config.levelMapSync(), + levelList = Object.keys(levelsMap); + + var defaultLevels = levelList.filter(function(level) { + return levelsMap[level].default; + }); + + var levelByCwd = levelList.filter(function(level) { + return cwd.indexOf(level) === 0; + }).sort().reverse()[0]; + + levels = levelByCwd || (defaultLevels.length ? defaultLevels : cwd); + } + + Array.isArray(entities) || (entities = [entities]); + Array.isArray(levels) || (levels = [levels]); + + return flatten(entities.map(function(input) { + var isFileGlob = typeof input === 'string'; + + return (isFileGlob ? braceExpansion(input) : [input]).map(function(filepathOrInput) { + var currentLevels = levels; + + if (typeof filepathOrInput === 'string') { + var currentLevel = path.dirname(filepathOrInput); + currentLevel !== '.' && (currentLevels = [currentLevel]); + } + + return currentLevels.map(function(relLevel) { + var rootDir = config.rootSync() || cwd, + level = path.resolve(rootDir, relLevel), + levelOptions = config.levelSync(level) || {}, + currentTechs = uniq([].concat(levelOptions.techs || [], techs)), + entity; + + if (isFileGlob) { + var file = path.basename(filepathOrInput), + // split for entity key and tech (by first dot) + match = file.match(/^([^.]+)(?:\.(.+))?$/); + + entity = bemNaming(levelOptions.naming).parse(match[1]); + if (match[2]) { + currentTechs = uniq(techs.concat(match[2])); + } + } else { + entity = BemEntityName.create(filepathOrInput); + } + + options.onlyTech && (currentTechs = options.onlyTech); + + options.excludeTech && (currentTechs = currentTechs.filter(function(tech) { + return options.excludeTech.indexOf(tech) === -1; + })); + + return currentTechs.map(function(tech) { + if (options.forceRewrite) { + levelOptions.forceRewrite = options.forceRewrite; + } + + return { + config: config, + levelOptions: levelOptions, + tech: tech, + options: options, + entity: entity, + level: level + }; + }); + }); + }); + })); +}; diff --git a/lib/get-path.js b/lib/get-path.js new file mode 100644 index 0000000..ef1b6f7 --- /dev/null +++ b/lib/get-path.js @@ -0,0 +1,15 @@ +const path = require('path'); +const scheme = require('@bem/fs-scheme'); +const BemCell = require('@bem/cell'); + +module.exports = function getPath(item) { + const levelOptions = item.config.levelSync(item.level) || {}; + const levelScheme = levelOptions.scheme; + const buildPath = scheme(levelScheme).path; + const pathToFile = buildPath( + new BemCell({ entity: item.entity, tech: item.tech }), + item.levelOptions.schemeOptions || item.levelOptions + ); + + return path.join(path.resolve(item.level), pathToFile); +}; diff --git a/lib/template.js b/lib/template.js index fd03c6b..65f191a 100644 --- a/lib/template.js +++ b/lib/template.js @@ -1,30 +1,44 @@ 'use strict'; var fs = require('fs'), - path = require('path'); + path = require('path'), + bemNaming = require('@bem/naming'), + stream = require('stream'), + streamToPromise = require('stream-to-promise'); function defaultTemplate() { return ''; } -module.exports = function getTemplate(tech, options) { - var templateFolder = options.templateFolder, - techId = (options.techsTemplates && options.techsTemplates[tech]) || tech, - templatePath = options.templates && options.templates[techId] && path.resolve(options.templates[techId]), - possiblePaths = [path.join(__dirname, 'templates')]; +module.exports = { + get: function(tech, options) { + var templateFolder = options.templateFolder, + techId = (options.techsTemplates && options.techsTemplates[tech]) || tech, + templatePath = options.templates && options.templates[techId] && path.resolve(options.templates[techId]), + possiblePaths = [path.join(__dirname, 'templates')]; - templateFolder && possiblePaths.unshift(templateFolder); + templateFolder && possiblePaths.unshift(templateFolder); - if (!templatePath) { - for (var i = 0; i < possiblePaths.length; i++) { - var possibleTemplatePath = path.resolve(possiblePaths[i], techId + '.js'); + if (!templatePath) { + for (var i = 0; i < possiblePaths.length; i++) { + var possibleTemplatePath = path.resolve(possiblePaths[i], techId + '.js'); - if (fs.existsSync(possibleTemplatePath)) { - templatePath = possibleTemplatePath; - break; + if (fs.existsSync(possibleTemplatePath)) { + templatePath = possibleTemplatePath; + break; + } } } - } - return templatePath ? require(templatePath) : defaultTemplate; + return templatePath ? require(templatePath) : defaultTemplate; + }, + + apply: function(item) { + var template = item.options.fileContent || this.get(item.tech, item.levelOptions), + isPiped = template instanceof stream.Readable; + + return isPiped ? streamToPromise(template) : Promise.resolve( + typeof template === 'string' ? template : template(item.entity, bemNaming(item.levelOptions.naming)) + ); + } }; diff --git a/package.json b/package.json index edad692..fe37c2f 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "brace-expansion": "^1.1.6", "mkdirp": "^0.5.1", "pinkie-promise": "^2.0.1", + "stream-to-promise": "^2.2.0", "uniq": "^1.0.1" }, "devDependencies": { diff --git a/test/create-entity.js b/test/create-entity.js new file mode 100644 index 0000000..38c067a --- /dev/null +++ b/test/create-entity.js @@ -0,0 +1,116 @@ +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const mkdirp = require('mkdirp'); +const rimraf = require('rimraf'); +const naming = require('@bem/naming'); +const EOL = require('os').EOL; +const assert = require('assert'); +const createEntity = require('../lib/create-entity'); + +const tmpDir = path.join(__dirname, 'tmp'); +const initialCwd = process.cwd(); + +const templates = { + css: function(entity, namingScheme) { + const className = typeof entity === 'string' ? entity : naming(namingScheme).stringify(entity); + + return [ + '.' + className + ' {', + ' ', + '}', + '' + ].join(EOL); + } +}; + +function testEntityHelper(fileName, content, options) { + return createEntity(fileName, content, options).then(() => { + let actualContent; + const relativeFileName = path.relative(initialCwd, fileName); + + if (typeof content === 'undefined') { + content = ''; + } + + try { + actualContent = fs.readFileSync(fileName, 'utf8'); + } catch (err) { + throw new Error(`${relativeFileName} was not created`); + } + + assert.equal(actualContent, content, `${relativeFileName} content is not correct`); + }); +} + +describe('bem-tools-create', () => { + beforeEach(() => mkdirp.sync(tmpDir)); + + afterEach(() => { + rimraf.sync(tmpDir); + process.chdir(initialCwd); + }); + + describe('default scheme and default naming', () => { + it('should create a block using `nested` scheme and default naming', done => { + testEntityHelper( + path.join(tmpDir, 'b', 'b.css'), + templates.css('b'), + {} + ).then(done); + }); + + it('should create an element using `nested` scheme and default naming', done => { + testEntityHelper( + path.join(tmpDir, 'b', '__e', 'b__e.css'), + templates.css('b__e'), + {} + ).then(done); + }); + + it('should create an block modifier using `nested` scheme and default naming', done => { + testEntityHelper( + path.join(tmpDir, 'b', '_m', 'b_m_v.css'), + templates.css('b_m_v'), + {} + ).then(done); + }); + + it('should create an element modifier using `nested` scheme and default naming', done => { + testEntityHelper( + path.join(tmpDir, 'b', '__e', '_em', 'b__e_em_ev.css'), + templates.css('b__e_em_ev'), + {} + ).then(done); + }); + }); + + describe('custom options', () => { + it('should create entities with naming from config', () => { + const entity = { block: 'b', elem: 'e1', modName: 'm1', modVal: 'v1' }; + const namingScheme = { + delims: { + elem: '-', + mod: { name: '--', val: '_' } + } + }; + + return testEntityHelper( + path.join(tmpDir, 'b', '-e1', '--m1', 'b-e1--m1_v1.css'), + templates.css(entity, namingScheme), + {} + ); + }); + + it('should create blocks with scheme from config', () => { + const entity = { block: 'b', elem: 'e1', modName: 'm1', modVal: 'v1' }; + + return testEntityHelper( + path.join(tmpDir, 'b__e1_m1_v1.css'), + templates.css(entity), + {} + ); + }); + }); +}); diff --git a/test/templates.js b/test/templates.js new file mode 100644 index 0000000..d4974db --- /dev/null +++ b/test/templates.js @@ -0,0 +1,146 @@ +'use strict'; + +const assert = require('assert'); +const bemNaming = require('@bem/naming'); + +const expected = { + 'default': { + 'bemhtml.js': { + block: "block('b').content()(function() {\n return;\n});\n", + element: "block('b').elem('e').content()(function() {\n return;\n});\n", + modifier: "block('b').mod('m', 'v').content()(function() {\n return;\n});\n", + 'element modifier': "block('b').elem('e').elemMod('em', 'ev').content()(function() {\n return;\n});\n" + }, + 'bemtree.js': { + block: "block('b').content()(function() {\n return;\n});\n", + element: "block('b').elem('e').content()(function() {\n return;\n});\n", + modifier: "block('b').mod('m', 'v').content()(function() {\n return;\n});\n", + 'element modifier': "block('b').elem('e').elemMod('em', 'ev').content()(function() {\n return;\n});\n" + }, + css: { + block: '.b {\n \n}\n', + element: '.b__e {\n \n}\n', + modifier: '.b_m_v {\n \n}\n', + 'element modifier': '.b__e_em_ev {\n \n}\n' + }, + js: { + block: "modules.define('b', ['i-bem-dom'], function(provide, bemDom) {\n\nprovide(bemDom.declBlock(this.name, {\n onSetMod: {\n js: {\n inited: function() {\n \n }\n }\n }\n}));\n\n});\n", + element: "modules.define('b__e', ['i-bem-dom'], function(provide, bemDom) {\n\nprovide(bemDom.declElem('b', 'e', {\n onSetMod: {\n js: {\n inited: function() {\n \n }\n }\n }\n}));\n\n});\n", + modifier: "modules.define('b', function(provide, B) {\n\nprovide(B.declMod({ modName: 'm', modVal: 'v' }, {\n onSetMod: {\n js: {\n inited: function() {\n \n }\n }\n }\n}));\n\n});\n", + 'element modifier': "modules.define('b__e', function(provide, B__e) {\n\nprovide(B__e.declMod({ modName: 'em', modVal: 'ev' }, {\n onSetMod: {\n js: {\n inited: function() {\n \n }\n }\n }\n}));\n\n});\n" + } + }, + + custom: { + 'bemhtml.js': { + block: "block('b').content()(function() {\n return;\n});\n", + element: "block('b').elem('e').content()(function() {\n return;\n});\n", + modifier: "block('b').mod('m', 'v').content()(function() {\n return;\n});\n", + 'element modifier': "block('b').elem('e').elemMod('em', 'ev').content()(function() {\n return;\n});\n" + }, + 'bemtree.js': { + block: "block('b').content()(function() {\n return;\n});\n", + element: "block('b').elem('e').content()(function() {\n return;\n});\n", + modifier: "block('b').mod('m', 'v').content()(function() {\n return;\n});\n", + 'element modifier': "block('b').elem('e').elemMod('em', 'ev').content()(function() {\n return;\n});\n" + }, + css: { + block: '.b {\n \n}\n', + element: '.b-e {\n \n}\n', + modifier: '.b--m_v {\n \n}\n', + 'element modifier': '.b-e--em_ev {\n \n}\n' + }, + js: { + block: "modules.define('b', ['i-bem-dom'], function(provide, bemDom) {\n\nprovide(bemDom.declBlock(this.name, {\n onSetMod: {\n js: {\n inited: function() {\n \n }\n }\n }\n}));\n\n});\n", + element: "modules.define('b-e', ['i-bem-dom'], function(provide, bemDom) {\n\nprovide(bemDom.declElem('b', 'e', {\n onSetMod: {\n js: {\n inited: function() {\n \n }\n }\n }\n}));\n\n});\n", + modifier: "modules.define('b', function(provide, B) {\n\nprovide(B.declMod({ modName: 'm', modVal: 'v' }, {\n onSetMod: {\n js: {\n inited: function() {\n \n }\n }\n }\n}));\n\n});\n", + 'element modifier': "modules.define('b-e', function(provide, BE) {\n\nprovide(BE.declMod({ modName: 'em', modVal: 'ev' }, {\n onSetMod: {\n js: {\n inited: function() {\n \n }\n }\n }\n}));\n\n});\n" + } + } +}; + +const selectors = { + block: { block: 'b' }, + element: { block: 'b', elem: 'e' }, + modifier: { block: 'b', modName: 'm', modVal: 'v' }, + 'element modifier': { block: 'b', elem: 'e', modName: 'em', modVal: 'ev' } +}; + +const namingSchemes = { + 'default': {}, + custom: { + delims: { + elem: '-', + mod: { name: '--', val: '_' } + } + } +}; + +Object.keys(namingSchemes).forEach(scheme => { + describe(`templates with ${scheme} scheme`, () => { + Object.keys(expected[scheme]).forEach(tmpl => { + describe(tmpl, () => { + const template = require(`../lib/templates/${tmpl}`); + Object.keys(selectors).forEach(selector => { + it(`provides content for ${selector}`, () => { + assert.equal( + template(selectors[selector], bemNaming(namingSchemes[scheme])), + expected[scheme][tmpl][selector] + ); + }); + }); + }); + }); + }); +}); + +describe('deps.js', () => { + it('provides content', () => { + const deps = require(`../lib/templates/deps.js`); + + assert.equal(deps(), '({\n shouldDeps: [\n \n ]\n})\n'); + }); +}); + +describe('template.apply', () => { + const template = require(`../lib/template`); + + it('should use template from templates directory', () => { + const item = { + levelOptions: { 'default': false }, + tech: 'css', + options: {}, + entity: { block: 'b' } + }; + + return template.apply(item).then(content => assert.equal(content, '.b {\n \n}\n')); + }); + + it('should use string fileContent if exists', () => { + const item = { + levelOptions: { 'default': false }, + tech: 'css', + options: { fileContent: 'test' }, + entity: { block: 'b' } + }; + + return template.apply(item).then(content => assert.equal(content, 'test')); + }); + + it('should use stream fileContent if exists', () => { + const Readable = require('stream').Readable; + const stream = new Readable; + + const item = { + levelOptions: { 'default': false }, + tech: 'css', + options: { fileContent: stream }, + entity: { block: 'b' } + }; + + stream.push('test'); + stream.push(null); + + return template.apply(item).then(content => assert.equal(content, 'test')); + }); +}); diff --git a/test/test.js b/test/test.js index a594224..6a56435 100644 --- a/test/test.js +++ b/test/test.js @@ -8,7 +8,6 @@ const create = require('..'); const naming = require('@bem/naming'); const EOL = require('os').EOL; const assert = require('assert'); -const stream = require('stream'); const tmpDir = path.join(__dirname, 'tmp'); const initialCwd = process.cwd(); @@ -798,20 +797,5 @@ describe('bem-tools-create', () => { }]) ); }); - - it('should support custom content with pipe', () => { - const content = 'Some piped testing content'; - const srcStream = new stream.Readable(); - srcStream.push(content); - srcStream.push(null); - - return testEntityHelper([{ block: 'b' }], [tmpDir], ['css'], - { fileContent: srcStream }, [{ - name: path.join(tmpDir, 'b', 'b.css'), - content: content - }] - ); - }); - }); }); diff --git a/test/test1.js b/test/test1.js new file mode 100644 index 0000000..f2b62f4 --- /dev/null +++ b/test/test1.js @@ -0,0 +1,789 @@ +'use strict'; + +const path = require('path'); +const mkdirp = require('mkdirp'); +const assert = require('assert'); +const getEntityData = require('../lib/get-entity-data'); +const getPath = require('../lib/get-path'); +const tmpDir = process.cwd(); + +function testEntityHelper(entities, levels, techs, options, expected) { + const actualPaths = getEntityData(entities, levels, techs, options) + .map(getPath) + .sort(); + const expectdPaths = expected + .map(item => item.path) + .sort(); + + assert.deepEqual(actualPaths, expectdPaths); +} + +describe('bem-tools-create', () => { + describe('default scheme and default naming', () => { + it('should create a block using `nested` scheme and default naming', () => { + return testEntityHelper([{ block: 'b' }], [tmpDir], ['css'], {}, [{ + path: path.join(tmpDir, 'b', 'b.css'), + options: {} + }]); + }); + + it('should create an element using `nested` scheme and default naming', () => { + return testEntityHelper([{ block: 'b', elem: 'e' }], [tmpDir], ['css'], {}, [{ + path: path.join(tmpDir, 'b', '__e', 'b__e.css'), + options: {} + }]); + }); + + it('should create an block modifier using `nested` scheme and default naming', () => { + return testEntityHelper([{ block: 'b', modName: 'm', modVal: 'v' }], [tmpDir], ['css'], {}, [{ + path: path.join(tmpDir, 'b', '_m', 'b_m_v.css'), + options: {} + }]); + }); + + it('should create an element modifier using `nested` scheme and default naming', () => { + return testEntityHelper([{ block: 'b', elem: 'e', modName: 'em', modVal: 'ev' }], [tmpDir], ['css'], {}, [{ + path: path.join(tmpDir, 'b', '__e', '_em', 'b__e_em_ev.css'), + options: {} + }]); + }); + + it('should create a block with different techs', () => { + return testEntityHelper([{ block: 'b' }], [tmpDir], ['css', 'deps.js'], {}, [ + { + path: path.join(tmpDir, 'b', 'b.css'), + options: {} + }, + { + path: path.join(tmpDir, 'b', 'b.deps.js'), + options: {} + } + ]); + }); + }); + + describe('custom options', () => { + it('should create entities with naming from config', () => { + const entity = { block: 'b', elem: 'e1', modName: 'm1', modVal: 'v1' }; + const namingScheme = { + delims: { + elem: '-', + mod: { name: '--', val: '_' } + } + }; + + return testEntityHelper([entity], [tmpDir], ['css'], { defaults: { naming: namingScheme } }, [{ + path: path.join(tmpDir, 'b', '-e1', '--m1', 'b-e1--m1_v1.css'), + options: { naming: namingScheme } + }]); + }); + + it('should create blocks with scheme from config', () => { + const entity = { block: 'b', elem: 'e1', modName: 'm1', modVal: 'v1' }; + + return testEntityHelper([entity], [tmpDir], ['css'], { defaults: { scheme: 'flat' } }, [{ + path: path.join(tmpDir, 'b__e1_m1_v1.css'), + options: { scheme: 'flat' } + }]); + }); + + describe('levels', () => { + it('should create a block on default levels from config', () => { + const opts = { + defaults: { levels: {} }, + fsRoot: tmpDir, + fsHome: tmpDir + }; + + ['level1', 'level2'].forEach(function(lvl) { + const level = path.join(tmpDir, lvl); + opts.defaults.levels[level] = { 'default': true }; + }); + + return testEntityHelper([{ block: 'b' }], null, ['css'], opts, [ + { + path: path.join(tmpDir, 'level1', 'b', 'b.css'), + options: { 'default': true } + }, + { + path: path.join(tmpDir, 'level2', 'b', 'b.css'), + options: { 'default': true } + } + ]); + }); + + it('should create entities on levels with provided config', () => { + const levels = [path.join(tmpDir, 'l1'), path.join(tmpDir, 'l2')]; + const entity = { block: 'b', elem: 'e1', modName: 'm1', modVal: 'v1' }; + const namingScheme = { + delims: { + elem: '-', + mod: { name: '--', val: '_' } + } + }; + const opts = { + defaults: { + levels: {} + } + }; + + opts.defaults.levels[levels[0]] = { + naming: namingScheme + }; + + opts.defaults.levels[levels[1]] = { + scheme: 'flat' + }; + + return testEntityHelper([entity], levels, ['css'], opts, [ + { + path: path.join(tmpDir, 'l1', 'b', '-e1', '--m1', 'b-e1--m1_v1.css'), + options: { naming: namingScheme } + }, + { + path: path.join(tmpDir, 'l2', 'b__e1_m1_v1.css'), + options: { scheme: 'flat' } + } + ]); + }); + + it('should bubble to parent level when cwd is inside an entity', () => { + const opts = { + defaults: { levels: {}, root: true, __source: path.join(tmpDir, '.bemrc') }, + fsRoot: tmpDir, + fsHome: tmpDir + }; + + ['level1', 'level2'].forEach(function(lvl) { + const level = path.join(tmpDir, lvl); + opts.defaults.levels[level] = { 'default': lvl === 'level2' }; + }); + + const fakeCwd = path.join(tmpDir, 'level1', 'b1', '__e1'); + mkdirp.sync(fakeCwd); + process.chdir(fakeCwd); + + return testEntityHelper([{ block: 'b' }], null, ['css'], opts, [ + { + path: path.join(tmpDir, 'level1', 'b', 'b.css'), + options: { 'default': false } + } + ]); + }); + + it('should create an entity on default level when cwd is not inside a level folder', () => { + const opts = { + defaults: { levels: {} }, + fsRoot: tmpDir, + fsHome: tmpDir + }; + + ['level1', 'level2'].forEach(function(lvl) { + const level = path.join(tmpDir, lvl); + opts.defaults.levels[level] = { 'default': true }; + }); + + const fakeCwd = path.join(tmpDir, 'some-folder', 'cwd'); + mkdirp.sync(fakeCwd); + process.chdir(fakeCwd); + + return testEntityHelper([{ block: 'b' }], null, ['css'], opts, [ + { + path: path.join(tmpDir, 'level1', 'b', 'b.css'), + options: { 'default': true } + }, + { + path: path.join(tmpDir, 'level2', 'b', 'b.css'), + options: { 'default': true } + } + ]); + }); + + it('should create an entity on provided not default level when cwd is not inside a level folder', () => { + const opts = { + defaults: { levels: {}, root: true, __source: path.join(tmpDir, '.bemrc') }, + fsRoot: tmpDir, + fsHome: tmpDir + }; + + ['level1', 'level2'].forEach(function(lvl) { + const level = path.join(tmpDir, lvl); + opts.defaults.levels[level] = { 'default': lvl === 'level1' }; + }); + + const fakeCwd = path.join(tmpDir, 'some-folder', 'cwd'); + mkdirp.sync(fakeCwd); + process.chdir(fakeCwd); + + return testEntityHelper([{ block: 'b' }], 'level2', ['css'], opts, [ + { + path: path.join(tmpDir, 'level2', 'b', 'b.css'), + options: { 'default': false } + } + ]); + }); + + it('should create a block on cwd as a fallback', () => { + const fakeCwd = path.join(tmpDir, 'cwd'); + mkdirp.sync(fakeCwd); + process.chdir(fakeCwd); + + return testEntityHelper([{ block: 'b' }], null, ['css'], { fsRoot: tmpDir, fsHome: tmpDir }, [{ + path: path.join(fakeCwd, 'b', 'b.css'), + options: {} + }]); + }); + + it('should create block on provided levels', () => { + return testEntityHelper([{ block: 'b' }], [tmpDir], ['css'], { fsRoot: tmpDir, fsHome: tmpDir }, [{ + path: path.join(tmpDir, 'b', 'b.css'), + options: {} + }]); + }); + + describe('level config in plugin config', () => { + it('should respect level techs', () => { + const createLevels = {}; + const opts = { + defaults: { + levels: {}, + modules: { + 'bem-tools': { + plugins: { + create: { + techs: ['common-create-tech1', 'common-create-tech2'], + levels: createLevels + } + } + } + } + }, + fsRoot: tmpDir, + fsHome: tmpDir + }; + + const level = path.join(tmpDir, 'level1'); + opts.defaults.levels[level] = { 'default': true }; + + createLevels[level] = { + techs: ['create-level-tech1'] + }; + + return testEntityHelper([{ block: 'b' }], null, ['tech1', 'tech2'], opts, [ + { + path: path.join(level, 'b', 'b.tech1'), + options: { + modules: opts.defaults.modules, + techs: ['create-level-tech1'], + 'default': true + } + }, + { + path: path.join(level, 'b', 'b.tech2'), + options: { + modules: opts.defaults.modules, + techs: ['create-level-tech1'], + 'default': true + } + }, + { + path: path.join(level, 'b', 'b.create-level-tech1'), + options: { + modules: opts.defaults.modules, + techs: ['create-level-tech1'], + 'default': true + } + } + ]); + }); + + it('should get default level from plugin config', () => { + const createLevels = {}; + const opts = { + defaults: { + levels: {}, + modules: { + 'bem-tools': { + plugins: { + create: { + techs: ['common-create-tech1', 'common-create-tech2'], + levels: createLevels + } + } + } + } + }, + fsRoot: tmpDir, + fsHome: tmpDir + }; + + const level = path.join(tmpDir, 'level1'); + + createLevels[level] = { + techs: ['create-level-tech1'], + 'default': true + }; + + return testEntityHelper([{ block: 'b' }], null, ['tech1', 'tech2'], opts, [ + { + path: path.join(level, 'b', 'b.tech1'), + options: { + modules: opts.defaults.modules, + techs: ['create-level-tech1'], + 'default': true + } + + }, + { + path: path.join(level, 'b', 'b.tech2'), + options: { + modules: opts.defaults.modules, + techs: ['create-level-tech1'], + 'default': true + } + + }, + { + path: path.join(level, 'b', 'b.create-level-tech1'), + options: { + modules: opts.defaults.modules, + techs: ['create-level-tech1'], + 'default': true + } + + } + ]); + }); + + it('should respect level templates', () => { + const createLevels = {}; + const opts = { + defaults: { + levels: {}, + modules: { + 'bem-tools': { + plugins: { + create: { + templates: { css: path.join(__dirname, 'tech-templates', 'css') }, + levels: createLevels + } + } + } + } + }, + fsRoot: tmpDir, + fsHome: tmpDir + }; + + const level = path.join(tmpDir, 'level1'); + + createLevels[level] = { + templates: { css: path.join(__dirname, 'tech-templates', 'css2') }, + 'default': true + }; + + return testEntityHelper([{ block: 'b' }], null, ['css'], opts, [ + { + path: path.join(level, 'b', 'b.css') + } + ]); + }); + + it('should support glob with absolute level path', () => { + const createPluginLevels = {}; + const opts = { + defaults: { + modules: { + 'bem-tools': { + plugins: { + create: { + techs: ['tech1', 'tech2'], + levels: createPluginLevels + } + } + } + } + }, + fsRoot: tmpDir, + fsHome: tmpDir + }; + + const level = path.join(tmpDir, '*.blocks'); + createPluginLevels[level] = { + techs: ['tech4', 'tech3'], + 'default': true + }; + + mkdirp.sync(path.join(tmpDir, 'common.blocks')); + mkdirp.sync(path.join(tmpDir, 'desktop.blocks')); + + return testEntityHelper([{ block: 'b' }], null, null, opts, [ + { + path: path.join(tmpDir, 'common.blocks', 'b', 'b.tech3') + }, + { + path: path.join(tmpDir, 'common.blocks', 'b', 'b.tech4') + }, + { + path: path.join(tmpDir, 'desktop.blocks', 'b', 'b.tech3') + }, + { + path: path.join(tmpDir, 'desktop.blocks', 'b', 'b.tech4') + } + ]); + }); + + it('should support glob resolution for levels', () => { + const levels = {}; + const createPluginLevels = {}; + const opts = { + defaults: { + levels, + modules: { + 'bem-tools': { + plugins: { + create: { + techs: ['tech1', 'tech2'], + levels: createPluginLevels + } + } + } + } + }, + fsRoot: tmpDir, + fsHome: tmpDir + }; + + const level = '*.blocks'; + levels[level] = { 'default': true }; + createPluginLevels[level] = { techs: ['tech4', 'tech3'] }; + + mkdirp.sync(path.join(tmpDir, 'common.blocks')); + mkdirp.sync(path.join(tmpDir, 'desktop.blocks')); + process.chdir(tmpDir); + + return testEntityHelper([{ block: 'b' }], null, null, opts, [ + { + path: path.join(tmpDir, 'common.blocks', 'b', 'b.tech3') + }, + { + path: path.join(tmpDir, 'common.blocks', 'b', 'b.tech4') + }, + { + path: path.join(tmpDir, 'desktop.blocks', 'b', 'b.tech3') + }, + { + path: path.join(tmpDir, 'desktop.blocks', 'b', 'b.tech4') + } + ]); + }); + }); + }); + + describe('techs', () => { + it('should create block in techs from config', () => { + const opts = { + defaults: { + modules: { + 'bem-tools': { + plugins: { + create: { + techs: ['tech1', 'tech2'] + } + } + } + } + }, + fsRoot: tmpDir, + fsHome: tmpDir + }; + + return testEntityHelper([{ block: 'b' }], [tmpDir], null, opts, [ + { + path: path.join(tmpDir, 'b', 'b.tech1') + }, + { + path: path.join(tmpDir, 'b', 'b.tech2') + } + ]); + }); + + it('should create block in techs from config and provided techs', () => { + const opts = { + defaults: { + modules: { + 'bem-tools': { + plugins: { + create: { + techs: ['tech1', 'tech2'] + } + } + } + } + }, + fsRoot: tmpDir, + fsHome: tmpDir + }; + + return testEntityHelper([{ block: 'b' }], [tmpDir], ['tech3', 'tech4'], opts, [ + { path: path.join(tmpDir, 'b', 'b.tech1') }, + { path: path.join(tmpDir, 'b', 'b.tech2') }, + { path: path.join(tmpDir, 'b', 'b.tech3') }, + { path: path.join(tmpDir, 'b', 'b.tech4') } + ]); + }); + + // TODO: check that it fires only twice instead of four times + it('should create block in techs from config and the same provided techs', () => { + const opts = { + defaults: { + modules: { + 'bem-tools': { + plugins: { + create: { + techs: ['tech1', 'tech2'] + } + } + } + } + }, + fsRoot: tmpDir, + fsHome: tmpDir + }; + + return testEntityHelper([{ block: 'b' }], [tmpDir], ['tech1', 'tech2'], opts, [ + { path: path.join(tmpDir, 'b', 'b.tech1') }, + { path: path.join(tmpDir, 'b', 'b.tech2') } + ]); + }); + + it('should create block only in provided techs', () => { + const opts = { + onlyTech: ['only1', 'only2'], + defaults: { + modules: { + 'bem-tools': { + plugins: { + create: { + techs: ['defTech1', 'defTech2'] + } + } + } + } + }, + fsRoot: tmpDir, + fsHome: tmpDir + }; + + return testEntityHelper([{ block: 'b' }], [tmpDir], ['tech1', 'tech2'], opts, [ + { path: path.join(tmpDir, 'b', 'b.only1') }, + { path: path.join(tmpDir, 'b', 'b.only2') } + ]); + }); + }); + + describe('template', () => { + it('should create a block using templates from config', () => { + const opts = { + defaults: { + modules: { + 'bem-tools': { + plugins: { + create: { + templates: { + css: path.join(__dirname, 'tech-templates', 'css') + } + } + } + } + } + }, + fsRoot: tmpDir, + fsHome: tmpDir + }; + + return testEntityHelper([{ block: 'b' }], [tmpDir], ['css'], opts, [{ + path: path.join(tmpDir, 'b', 'b.css') + }]); + }); + + it('should create a block using template ID', () => { + const opts = { + defaults: { + modules: { + 'bem-tools': { + plugins: { + create: { + techsTemplates: { + 'bemtree.js': 'bemhtml.js' + } + } + } + } + } + }, + fsRoot: tmpDir, + fsHome: tmpDir + }; + + return testEntityHelper([{ block: 'b' }], [tmpDir], ['bemtree.js'], opts, [{ + path: path.join(tmpDir, 'b', 'b.bemtree.js') + }]); + }); + }); + }); + + describe('string parsing', () => { + describe('entity parsing', () => { + it('should parse block from string with techs from args', () => { + return testEntityHelper('b1', tmpDir, ['t1', 't2'], {}, [ + { path: path.join(tmpDir, 'b1', 'b1.t1') }, + { path: path.join(tmpDir, 'b1', 'b1.t2') } + ]); + }); + + it('should parse block from string with techs from config', () => { + const opts = { + defaults: { + modules: { + 'bem-tools': { + plugins: { + create: { + techs: ['tech1', 'tech2'] + } + } + } + } + }, + fsRoot: tmpDir, + fsHome: tmpDir + }; + + return testEntityHelper('b1', tmpDir, null, opts, [ + { path: path.join(tmpDir, 'b1', 'b1.tech1') }, + { path: path.join(tmpDir, 'b1', 'b1.tech2') } + ]); + }); + + it('should parse block with a tech from a string and ignore techs from config', () => { + const opts = { + defaults: { + modules: { + 'bem-tools': { + plugins: { + create: { + techs: ['tech1', 'tech2'] + } + } + } + } + }, + fsRoot: tmpDir, + fsHome: tmpDir + }; + + return testEntityHelper('b1.css', tmpDir, ['argTech'], opts, [ + { + path: path.join(tmpDir, 'b1', 'b1.css') + }, + { path: path.join(tmpDir, 'b1', 'b1.argTech') } + ]); + }); + + it('should parse elem from string', () => { + return testEntityHelper('b1__e1', tmpDir, ['t1'], {}, [ + { path: path.join(tmpDir, 'b1', '__e1', 'b1__e1.t1') } + ]); + }); + + it('should parse block mod from string', () => { + return testEntityHelper('b1_m1', tmpDir, ['t1'], {}, [ + { path: path.join(tmpDir, 'b1', '_m1', 'b1_m1.t1') } + ]); + }); + + it('should parse block modVal from string', () => { + return testEntityHelper('b1_m1_v1', tmpDir, ['t1'], {}, [ + { path: path.join(tmpDir, 'b1', '_m1', 'b1_m1_v1.t1') } + ]); + }); + + it('should parse elem mod from string', () => { + return testEntityHelper('b1__e1_m1_v1', tmpDir, ['t1'], {}, [ + { path: path.join(tmpDir, 'b1', '__e1', '_m1', 'b1__e1_m1_v1.t1') } + ]); + }); + }); + + describe('levels from string', () => { + it('should get level from string', () => { + return testEntityHelper(tmpDir + '/level1/b1.t1', null, null, {}, [ + { path: path.join(tmpDir, 'level1', 'b1', 'b1.t1') } + ]); + }); + + it('should resolve level from string by config', () => { + const opts = { + defaults: { levels: {}, root: true, __source: path.join(tmpDir, '.bemrc') }, + fsRoot: tmpDir, + fsHome: tmpDir + }; + + ['level1', 'level2'].forEach(function(lvl) { + const level = path.join(tmpDir, lvl); + opts.defaults.levels[level] = { 'default': lvl === 'level1' }; + }); + + const fakeCwd = path.join(tmpDir, 'some-folder', 'cwd'); + mkdirp.sync(fakeCwd); + process.chdir(fakeCwd); + + return testEntityHelper(tmpDir + '/level1/b1.t1', null, null, opts, [ + { + path: path.join(tmpDir, 'level1', 'b1', 'b1.t1') + } + ]); + }); + }); + + it('should expand braces', () => { + return testEntityHelper('{b1,b2}.{t1,t2}', tmpDir, null, {}, [ + { path: path.join(tmpDir, 'b1', 'b1.t1') }, + { path: path.join(tmpDir, 'b1', 'b1.t2') }, + { path: path.join(tmpDir, 'b2', 'b2.t1') }, + { path: path.join(tmpDir, 'b2', 'b2.t2') } + ]); + }); + }); + + describe('respect context', () => { + it.skip('should get block from context', () => { + + }); + + it.skip('should get block and elem from context', () => { + + }); + + it.skip('should get modName from context', () => { + + }); + + // modVal if cwd is inside mod + }); + + describe('command line arguments support', () => { + it('should exclude tech', () => { + const excludedTechs = ['css', 'js']; + return testEntityHelper( + [{ block: 'b' }], + [tmpDir], + ['css', 'js', 't1'], + { excludeTech: excludedTechs }, + [{ + path: path.join(tmpDir, 'b', 'b.t1') + }] + ); + }); + }); +});