diff --git a/.gitignore b/.gitignore index ba2a97b..648ea07 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules coverage +npm-debug.log diff --git a/README.md b/README.md index 22cef2c..7f78cd7 100644 --- a/README.md +++ b/README.md @@ -121,6 +121,17 @@ module.exports = function(config) { * [в браузере](#Исполнение-шаблонов-в-браузере) * [для сборки HTML](#Использование-шаблонов-для-сборки-html) +### Способы экспортирования бандла шаблонов + +Существуют разные способы сборки: + +* в виде CommonJS-модуля для [исполнения шаблонов в Node.js](#Исполнение-шаблонов-в-nodejs) или сборки с Browserify +* в виде YM-модуля для [исполнения шаблонов в браузере](#Исполнение-шаблонов-в-браузере) + +По умолчанию сборщик пытается собрать шаблоны как CommonJS- и YM-модуль одновременно (будет возможно подключать как в среде браузера с модульной системой, так и в среде Node.js) и, если файл подключается иным способом, то при его исполнении объект с шаблонизатором экспортируется в глобальную область видимости. + +Это поведение описывается объектом опции `exports: { globals: true, commonJS: true, ym: true }` и является значением по умолчанию. В свойстве `globals` допустимо передать строку `'force'` для явного экспорта объекта с шаблонизатором в глобальную область видимости. + ### Исполнение шаблонов в Node.js Скомпилированный файл подключается как модуль в формате [CommonJS](http://www.commonjs.org/). @@ -148,14 +159,19 @@ var bemjson = BEMTREE.apply({ block: 'page', data: { /* ... */ } }), Шаблоны доступны из глобальной переменной `BEMTREE` или `BEMHTML`. + Значение опции `exports`: `{ globals: true }`. + ```js var bemjson = BEMTREE.apply({ block: 'page', data: { /* ... */ } }), html = BEMHTML.apply(bemjson); // ... ``` + * **С модульной системой YModules** Шаблоны доступны из модульной системы ([YModules](https://ru.bem.info/tools/bem/modules/)): + Значение опции `exports`: `{ ym: true }`. + ```js modules.require(['BEMTREE', 'BEMHTML'], function(BEMTREE, BEMHTML) { var bemjson = BEMTREE.apply({ block: 'page', data: { /* ... */ } }), diff --git a/api.ru.md b/api.ru.md index 038b4d9..fc1fcc1 100644 --- a/api.ru.md +++ b/api.ru.md @@ -25,6 +25,7 @@ bemhtml * [sourceSuffixes](#sourcesuffixes) * [requires](#requires) * [exportName](#exportname) +* [exports](#exports) * [forceBaseTemplates](#forceBaseTemplates) * [engineOptions](#engineoptions) * [naming](#naming) @@ -83,6 +84,24 @@ bemhtml }); ``` +#### exports + +Тип: `{globals: ?(Boolean|'force'), commonJS: ?Boolean, ym: ?Boolean}`. + +Указывает каким именно образом должен экспортироваться экземпляр BEMHTML. + +По умолчанию: `{ globals: true, commonJS: true, ym: true }`. + +Доступны следующие параметры: +* `globals` указывает выгружать ли ссылку на экземпляр в глобальную область видимости, игнорируется, если указана одна из модульных систем, со значением `'force'` экспортируется безусловно; +* `ym` — регистрировать ли экземпляр как модуль в YModules (при наличии переменной modules); +* `commonJS` — записывать ли экземпляр в `modules.exports` (при его наличии). + +Примеры использования: +- В случае переходного периода с глобального объекта к YModules можно передать `{globals: 'force', ym: true}` и шаблонизатор будет экспортирован для использования и из YModules, и из глобальной области видимости. +- Передав `{ym: true}` или `{commonJS: true}` можно запретить экспорт в глобальную область видимости. +- Передав `{ym: true, commonJS}` можно собрать бандл и для node.js, и для браузера с YModules, но без экспорта в глобальную область видимости. + #### forceBaseTemplates Тип: `Boolean`. По умолчанию `false`. diff --git a/lib/assets/bundle.jst b/lib/assets/bundle.jst index 9eaef5d..304cdf1 100644 --- a/lib/assets/bundle.jst +++ b/lib/assets/bundle.jst @@ -14,13 +14,13 @@ var ${ exportName }; var defineAsGlobal = true; // Provide with CommonJS - if (typeof module === 'object' && typeof module.exports === 'object') { - exports['${ exportName }'] = buildBemXjst(${ commonJSDependencies }); + if (${ exportsOption.commonJS === true } && typeof module === 'object' && typeof module.exports === 'object') { + module.exports['${ exportName }'] = buildBemXjst(${ commonJSDependencies }); defineAsGlobal = false; } // Provide to YModules - if (typeof modules === 'object') { + if (${ exportsOption.ym === true } && typeof modules === 'object') { modules.define( '${ exportName }', [<%_.each(ymDependencyNames, function(name) {%>'${ name }',<%});%>], @@ -31,12 +31,11 @@ var ${ exportName }; provide(buildBemXjst(${ ymDependencies })); } ); - defineAsGlobal = false; } // Provide to global scope - if (defineAsGlobal) { + if (${ exportsOption.globals === true } && defineAsGlobal || ${ exportsOption.globals === 'force' }) { ${ exportName } = buildBemXjst(${ globalDependencies }); global['${ exportName }'] = ${ exportName }; } diff --git a/lib/bundle.js b/lib/bundle.js index c473632..0837433 100644 --- a/lib/bundle.js +++ b/lib/bundle.js @@ -12,6 +12,7 @@ var compileCommonJS = require('./compile-commonjs'), * @param {String} opts.dirname Path to a directory with compiled file. * @param {String} [options.exportName=BEMHTML] Name for exports. * @param {Object} [options.requires={}] Names for dependencies. + * @param {Object} [options.exports={globals: true, commonJS: true, ym: true}] Export settings. * @returns {String} */ exports.compile = function (code, options) { @@ -24,6 +25,7 @@ exports.compile = function (code, options) { return template(code, { exportName: options.exportName, requires: requires, + exports: options.exports, commonJSModules: commonJSModules }); }); diff --git a/lib/templates/bundle.js b/lib/templates/bundle.js index 8d0615b..e19060f 100644 --- a/lib/templates/bundle.js +++ b/lib/templates/bundle.js @@ -21,6 +21,7 @@ _.mapKeys(templates, function (template, name) { * @param {Object} options Options. * @param {String} [options.exportName=BEMHTML] Name for exports. * @param {Object} [options.requires={}] Names for dependencies. + * @param {Object} [options.exports={globals: true, commonJS: true, ym: true}] Export settings. * @param {String} [options.commonJSModules] Code of CommonJS modules: require function * compiled with `browserify`. * @returns {String} @@ -29,6 +30,7 @@ module.exports = function (code, options) { options || (options = {}); var requires = options.requires || {}, + exports = options.exports || { globals: true, commonJS: true, ym: true }, templateOpts = { requires: requires }, ymDependencyNames = [], ymDependencyVars = []; @@ -43,6 +45,7 @@ module.exports = function (code, options) { return templates.bundle({ exportName: options.exportName || 'BEMHTML', bemxjst: code, + exportsOption: exports, commonJSModules: options.commonJSModules, globalDependencies: templates.globals(templateOpts), diff --git a/package.json b/package.json index 6156eff..5e46a31 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "enb-async-require": "1.0.1", "enb-require-or-eval": "1.0.2", "lodash": "4.13.1", + "node-eval": "1.0.2", "vow": "0.4.12", "vow-node": "0.3.0" }, diff --git a/techs/bem-xjst.js b/techs/bem-xjst.js index c77a382..1657a1b 100644 --- a/techs/bem-xjst.js +++ b/techs/bem-xjst.js @@ -12,14 +12,20 @@ var EOL = require('os').EOL, * * Compiles BEMXJST template files with BEMXJST translator and merges them into a single template bundle.

* - * Important: Normally you don't need to use this tech directly. + * Important: Usually you don't need to use this tech directly. * * @param {Object} [options] Options * @param {String} [options.target='?.bem-xjst.js'] Path to a target with compiled file. + * @param {Object} [options.exports={globals: true, commonJS: true, ym: true}] Export settings. */ module.exports = buildFlow.create() .name('bem-xjst') .target('target', '?.bem-xjst.js') + .defineOption('exports', { + globals: true, + commonJS: true, + ym: true + }) .methods({ /** * Returns filenames to compile. @@ -146,7 +152,8 @@ module.exports = buildFlow.create() return bundle.compile(compiledCode, { dirname: this.node.getDir(), exportName: this._exportName, - requires: this._requires + requires: this._requires, + exports: this._exports }); }, this); }, diff --git a/techs/bemhtml.js b/techs/bemhtml.js index 7d53824..0caf5cb 100644 --- a/techs/bemhtml.js +++ b/techs/bemhtml.js @@ -24,6 +24,7 @@ var path = require('path'); * Default as `__`. * * String `mod` — separates names and values of modifiers * from blocks and elements. Default as `_`. + * @param {Object} [options.exports={globals: true, commonJS: true, ym: true}] Export settings. * * @example * var BemhtmlTech = require('enb-bemxjst/techs/bemhtml'), diff --git a/techs/bemtree.js b/techs/bemtree.js index 283a784..09511aa 100644 --- a/techs/bemtree.js +++ b/techs/bemtree.js @@ -17,6 +17,7 @@ var path = require('path'); * @param {Object} [options.requires] Names of dependencies which should be available from * code of templates. * @param {Boolean} [options.forceBaseTemplates=false] Include base templates if no user templates present + * @param {Object} [options.exports={globals: true, commonJS: true, ym: true}] Export settings. * * @example * var BemtreeTech = require('enb-bemxjst/techs/bemtree'), diff --git a/test/techs/bemhtml.test.js b/test/techs/bemhtml.test.js index 47694b2..3f214bc 100644 --- a/test/techs/bemhtml.test.js +++ b/test/techs/bemhtml.test.js @@ -1,6 +1,7 @@ var fs = require('fs'), path = require('path'), mock = require('mock-fs'), + safeEval = require('node-eval'), MockNode = require('mock-enb/lib/mock-node'), Tech = require('../../techs/bemhtml'), loadDirSync = require('mock-enb/utils/dir-utils').loadDirSync, @@ -205,6 +206,74 @@ describe('bemhtml', function () { }); }); }); + + describe('with option `exports`', function () { + it('must export ym only', function () { + var modules = {}; + return build(['block("a").tag()("a")'], { + exports: { ym: true }, + context: { + module: undefined, + modules: { + define: function (name, _, p) { + p(function (res) { modules[name] = res; }); + } + } + } + }) + .spread(function () { + modules.BEMHTML.apply({ block: 'a' }) + .must.eql(''); + }); + }); + + it('must export commonjs only', function () { + var m = { exports: {} }; + return build(['block("a").tag()("a")'], { + exports: { commonJS: true }, + context: { module: m } + }) + .spread(function () { + m.exports.BEMHTML.apply({ block: 'a' }) + .must.eql(''); + }); + }); + + it('must export global only', function () { + var window = {}; + return build(['block("a").tag()("a")'], { + exports: { globals: true }, + context: { module: undefined, window: window } + }) + .spread(function () { + window.BEMHTML.apply({ block: 'a' }) + .must.eql(''); + }); + }); + + it('must export as ym and global', function () { + var modules = {}, window = {}; + return build(['block("a").tag()("a")'], { + exports: { globals: 'force', ym: true }, + context: { + module: undefined, + window: window, + modules: { + define: function (name, _, p) { + p(function (res) { modules[name] = res; }); + } + } + } + }) + .spread(function () { + modules.BEMHTML.apply({ block: 'a' }) + .must.eql('', 'modules failed'); + window.BEMHTML.apply({ block: 'a' }) + .must.eql('', 'global failed'); + }); + }); + // return build(templates, { exports: {globals: true, commonJS: true, ym: true} }) + }); }); function build(templates, options) { @@ -215,6 +284,7 @@ function build(templates, options) { blocks: {}, bundle: {} }, + noEval = typeof options.context === 'object', bundle, fileList; // hack for mock-fs @@ -230,12 +300,6 @@ function build(templates, options) { scheme.blocks = templates; } - if (templates.length) { - templates.forEach(function (item, i) { - scheme.blocks['block-' + i + '.bemhtml.js'] = item; - }); - } - mock(scheme); bundle = new MockNode('bundle'); @@ -243,11 +307,15 @@ function build(templates, options) { fileList.addFiles(loadDirSync('blocks')); bundle.provideTechData('?.files', fileList); - return bundle.runTechAndRequire(Tech, options) + return bundle[noEval ? 'runTech' : 'runTechAndRequire'](Tech, options) .spread(function (res) { var filename = bundle.resolvePath(bundle.unmaskTargetName(options.target || '?.bemhtml.js')), str = fs.readFileSync(filename, 'utf-8'); + if (noEval) { + res = safeEval(str, filename, options.context); + } + return [res, str]; }); }