diff --git a/README.md b/README.md index 266cb13..fcb3b0c 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ npm install --save-dev enb-bem-i18n * [i18n-keysets-xml](#i18n-keysets-xml) * [i18n-lang-js](#i18n-lang-js) -* [i18n-merge-keysets](#i18n-merge-keysets) +* [keysets](#keysets) * [i18n-bemjson-to-html](#i18n-bemjson-to-html) ### i18n-lang-js @@ -48,7 +48,7 @@ nodeConfig.addTechs([ ]); ``` -### i18n-merge-keysets +### keysets Собирает `?.keysets.<язык>.js`-файлы на основе `*.i18n`-папок для указанных языков. @@ -61,8 +61,7 @@ nodeConfig.addTechs([ ```javascript nodeConfig.addTechs([ - [ require('enb-bem-i18n/techs/i18n-merge-keysets'), { lang: 'all' } ], - [ require('enb-bem-i18n/techs/i18n-merge-keysets'), { lang: '{lang}' } ] + [ require('enb-bem-i18n/techs/keysets'), { lang: '{lang}' } ] ]); ``` diff --git a/package.json b/package.json index eeac5be..c97618a 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,8 @@ }, "dependencies": { "dom-js": "0.0.9", - "vow": "0.4.9" + "vow": "0.4.9", + "serialize-javascript": "1.0.0" }, "devDependencies": { "enb": ">= 0.11.0 < 1.0.0", diff --git a/techs/i18n-merge-keysets.js b/techs/i18n-merge-keysets.js deleted file mode 100644 index dbbbb8a..0000000 --- a/techs/i18n-merge-keysets.js +++ /dev/null @@ -1,83 +0,0 @@ -/** - * i18n-merge-keysets - * ================== - * - * Собирает `?.keysets.<язык>.js`-файлы на основе `*.i18n`-папок для указанных языков. - * - * **Опции** - * - * * *String* **target** — Результирующий таргет. По умолчанию — `?.keysets.{lang}.js`. - * * *String* **lang** — Язык, для которого небходимо собрать файл. - * - * **Пример** - * - * ```javascript - * nodeConfig.addTechs([ - * [ require('enb-bem-i18n/techs/i18n-merge-keysets'), { lang: 'all' } ], - * [ require('enb-bem-i18n/techs/i18n-merge-keysets'), { lang: '{lang}' } ] - * ]); - * ``` - */ -var vow = require('vow'), - asyncRequire = require('enb/lib/fs/async-require'), - dropRequireCache = require('enb/lib/fs/drop-require-cache'); - -module.exports = require('enb/lib/build-flow.js').create() - .name('i18n-merge-keysets') - .defineRequiredOption('lang') - .useDirList('i18n') - .target('target', '?.keysets.{lang}.js') - .builder(function (langKeysetDirs) { - var lang = this._lang, - langJs = lang + '.js', - langKeysetFiles = [].concat.apply([], langKeysetDirs.map(function (dir) { - return dir.files; - })).filter(function (fileInfo) { - return fileInfo.name === langJs; - }), - node = this.node, - cache = node.getNodeCache(this._target), - result = {}; - - return vow.all(langKeysetFiles.map(function (keysetFile) { - var filename = keysetFile.fullname, - basename = keysetFile.name, - cacheKey = 'keyset-file-' + basename, - promise; - - if (cache.needRebuildFile(cacheKey, filename)) { - dropRequireCache(require, filename); - promise = asyncRequire(filename) - .then(function (keysets) { - cache.cacheFileInfo(cacheKey, filename); - - return keysets; - }); - } else { - promise = asyncRequire(filename); - } - - promise.then(function (keysets) { - if (lang === 'all') { // XXX: Why the hell they break the pattern? - keysets = keysets.all || {}; - } - Object.keys(keysets).forEach(function (keysetName) { - var keyset = keysets[keysetName]; - result[keysetName] = (result[keysetName] || {}); - if (typeof keyset !== 'string') { - Object.keys(keyset).forEach(function (keyName) { - result[keysetName][keyName] = keyset[keyName]; - }); - } else { - result[keysetName] = keyset; - } - }); - }); - - return promise; - })) - .then(function () { - return 'module.exports = ' + JSON.stringify(result) + ';'; - }); - }) - .createTech(); diff --git a/techs/keysets.js b/techs/keysets.js new file mode 100644 index 0000000..bd4489c --- /dev/null +++ b/techs/keysets.js @@ -0,0 +1,93 @@ +/** + * keysets + * ================== + * + * Собирает `?.keysets.<язык>.js`-файлы на основе `*.i18n`-папок для указанных языков. + * + * **Опции** + * + * * *String* **target** — Результирующий таргет. По умолчанию — `?.keysets.{lang}.js`. + * * *String* **lang** — Язык, для которого небходимо собрать файл. + * + * **Пример** + * + * ```javascript + * nodeConfig.addTechs([ + * [ require('enb-bem-i18n/techs/keysets'), { lang: '{lang}' } ] + * ]); + * ``` + */ + +var path = require('path'), + format = require('util').format, + vow = require('vow'), + serialize = require('serialize-javascript'), + asyncRequire = require('enb/lib/fs/async-require'), + dropRequireCache = require('enb/lib/fs/drop-require-cache'); + +module.exports = require('enb/lib/build-flow.js').create() + .name('i18n-merge-keysets') + .target('target', '?.keysets.{lang}.js') + .defineRequiredOption('lang') + .useFileList(['i18n.js']) + .useDirList(['i18n']) + .builder(function (i18nFiles, i18nDirs) { + var node = this.node, + cache = node.getNodeCache(this._target), + lang = this._lang, + langname = lang + '.js', + result = {}, + files = [].concat(i18nFiles).concat(i18nDirs + .reduce(function (prev, dir) { + return prev.concat(dir.files); + }, []) + .filter(function (file) { + return [langname, 'all.js'].indexOf(file.name) > -1; + }) + .sort(function (file1, file2) { + if (file1.suffix === 'all.js') { + return -1; + } + if (file2.suffix === 'all.js') { + return 1; + } + return 0; + }) + ); + + return vow.all(files.map(function (file) { + var promise, + filename = file.fullname, + cacheKey = (function (f) { + var relative = path.relative(process.cwd(), f); + return format('keyset-file-%s', relative); + })(filename); + + if (cache.needRebuildFile(cacheKey, filename)) { + dropRequireCache(require, filename); + promise = asyncRequire(filename) + .then(function (keysets) { + cache.cacheFileInfo(cacheKey, filename); + return keysets; + }); + } else { + promise = asyncRequire(filename); + } + + return promise + .then(function (keysets) { + Object.keys(keysets).forEach(function (scope) { + var keyset = keysets[scope]; + + result[scope] || (result[scope] = {}); + Object.keys(keyset).forEach(function (name) { + result[scope][name] = keyset[name]; + }); + }); + }); + })) + .then(function () { + return 'module.exports = ' + serialize(result) + ';'; + }); + }) + .createTech(); diff --git a/test/techs/i18n-merge-keysets.test.js b/test/techs/i18n-merge-keysets.test.js deleted file mode 100644 index 4ae3c48..0000000 --- a/test/techs/i18n-merge-keysets.test.js +++ /dev/null @@ -1,302 +0,0 @@ -var fs = require('fs'), - path = require('path'), - mockFs = require('mock-fs'), - MockNode = require('mock-enb/lib/mock-node'), - FileList = require('enb/lib/file-list'), - dropRequireCache = require('enb/lib/fs/drop-require-cache'), - Tech = require('../../techs/i18n-merge-keysets'); - -describe('i18n-merge-keysets', function () { - afterEach(function () { - mockFs.restore(); - }); - - it('must get keyset from lang file', function () { - var keysets = [ - { - 'lang.js': { - scope: { - key: 'val' - } - } - } - ], - expected = { - scope: { - key: 'val' - } - }; - - return build(keysets, 'lang') - .then(function (res) { - res.must.eql(expected); - }); - }); - - it('must get keyset by lang', function () { - var keysets = [ - { - 'ru.js': { - 'ru-scope': { - key: 'val' - } - }, - 'en.js': { - 'ru-scope': { - key: 'val' - } - } - } - ], - expected = { - 'ru-scope': { - key: 'val' - } - }; - - return build(keysets, 'ru') - .then(function (res) { - res.must.eql(expected); - }); - }); - - it('must override value', function () { - var keysets = [ - { - 'lang.js': { - scope: { - key: 'val' - } - } - }, - { - 'lang.js': { - scope: { - key: 'val-2' - } - } - } - ], - expected = { - scope: { - key: 'val-2' - } - }; - - return build(keysets, 'lang') - .then(function (res) { - res.must.eql(expected); - }); - }); - - it('must merge keys', function () { - var keysets = [ - { - 'lang.js': { - scope: { - 'key-1': 'val' - } - } - }, - { - 'lang.js': { - scope: { - 'key-2': 'val' - } - } - } - ], - expected = { - scope: { - 'key-1': 'val', - 'key-2': 'val' - } - }; - - return build(keysets, 'lang') - .then(function (res) { - res.must.eql(expected); - }); - }); - - it('must merge scopes', function () { - var keysets = [ - { - 'lang.js': { - 'scope-1': { - key: 'val-1' - } - } - }, - { - 'lang.js': { - 'scope-2': { - key: 'val-2' - } - } - } - ], - expected = { - 'scope-1': { - key: 'val-1' - }, - 'scope-2': { - key: 'val-2' - } - }; - - return build(keysets, 'lang') - .then(function (res) { - res.must.eql(expected); - }); - }); - - it('must provide core', function () { - var keysets = [ - { - 'all.js': { - all: { - '': 'core' - } - } - } - ]; - - return build(keysets, 'all') - .then(function (res) { - res[''].must.be('core'); - }); - }); - - describe('cache', function () { - it('must get keyset from cache', function () { - var time = new Date(1); - - mockFs({ - 'block.i18n': { - 'lang.js': mockFs.file({ - content: 'module.exports = { scope: { key: "val" } };', - mtime: time - }) - }, - bundle: {} - }); - - var bundle = new MockNode('bundle'), - cache = bundle.getNodeCache('bundle.keysets.lang.js'), - fileList = new FileList(), - dirname = path.resolve('block.i18n'), - filename = path.join(dirname, 'lang.js'), - info = FileList.getFileInfo(dirname); - - info.files = [FileList.getFileInfo(filename)]; - fileList.addFiles([info]); - - bundle.provideTechData('?.dirs', fileList); - - dropRequireCache(require, filename); - require(filename); - cache.cacheFileInfo('keyset-file-lang.js', filename); - - mockFs({ - 'block.i18n': { - 'lang.js': mockFs.file({ - content: 'module.exports = { scope: { key: "val2" } };', - mtime: time - }) - }, - bundle: {} - }); - - return bundle.runTechAndRequire(Tech, { lang: 'lang' }) - .spread(function (res) { - res.must.eql({ scope: { key: 'val' } }); - }); - }); - - it('must ignore outdated cache', function () { - mockFs({ - 'block.i18n': { - 'lang.js': mockFs.file({ - content: 'module.exports = { scope: { key: "val" } };', - mtime: new Date(1) - }) - }, - bundle: {} - }); - - var bundle = new MockNode('bundle'), - cache = bundle.getNodeCache('bundle.keysets.lang.js'), - fileList = new FileList(), - dirname = path.resolve('block.i18n'), - filename = path.join(dirname, 'lang.js'), - info = FileList.getFileInfo(dirname); - - info.files = [FileList.getFileInfo(filename)]; - fileList.addFiles([info]); - - bundle.provideTechData('?.dirs', fileList); - - dropRequireCache(require, filename); - require(filename); - cache.cacheFileInfo('keyset-file-lang.js', filename); - - mockFs({ - 'block.i18n': { - 'lang.js': mockFs.file({ - content: 'module.exports = { scope: { key: "val2" } };', - mtime: new Date(2) - }) - }, - bundle: {} - }); - - return bundle.runTechAndRequire(Tech, { lang: 'lang' }) - .spread(function (res) { - res.must.eql({ scope: { key: 'val2' } }); - }); - }); - }); -}); - -function build(keysets, lang) { - var fsScheme = { - bundle: {} - }; - - for (var i = 0; i < keysets.length; ++i) { - var keyset = keysets[i], - basename = Object.keys(keyset)[0], - data = keyset[basename], - file = {}; - - file[basename] = 'module.exports = ' + JSON.stringify(data) + ';'; - - fsScheme[i + '.i18n'] = file; - } - - mockFs(fsScheme); - - var bundle = new MockNode('bundle'), - fileList = new FileList(), - dirs = keysets.map(function (keyset, i) { - var dirname = path.resolve(i + '.i18n'), - info = FileList.getFileInfo(dirname); - - info.files = fs.readdirSync(dirname).map(function (basename) { - var filename = path.join(dirname, basename); - - return FileList.getFileInfo(filename); - }); - - return info; - }); - - fileList.addFiles(dirs); - bundle.provideTechData('?.dirs', fileList); - - return bundle.runTechAndRequire(Tech, { lang: lang }) - .spread(function (res) { - return res; - }); -} diff --git a/test/techs/keysets.test.js b/test/techs/keysets.test.js new file mode 100644 index 0000000..4c2228f --- /dev/null +++ b/test/techs/keysets.test.js @@ -0,0 +1,710 @@ +var fs = require('fs'), + path = require('path'), + inspect = require('util').inspect, + mock = require('mock-fs'), + serializeJS = require('serialize-javascript'), + TestNode = require('enb/lib/test/mocks/test-node'), + FileList = require('enb/lib/file-list'), + dropRequireCache = require('enb/lib/fs/drop-require-cache'), + Tech = require('../../techs/keysets'); + +describe('i18n-merge-keysets', function () { + afterEach(function () { + mock.restore(); + }); + + describe('`i18n` dirs', function () { + it('must get empty keyset if empty dir', function () { + var keysets = { + 'block.i18n': {} + }, + expected = {}; + + return assert(keysets, expected, { lang: 'lang' }); + }); + + it('must get empty keyset if lang file', function () { + var keysets = { + 'block.i18n': { + 'lang.js': '' + } + }, + expected = {}; + + return assert(keysets, expected, { lang: 'lang' }); + }); + + it('must get keyset', function () { + var keysets = { + 'block.i18n': { + 'lang.js': serialize({ + scope: { + key: 'val' + } + }) + } + }, + expected = { + scope: { + key: 'val' + } + }; + + return assert(keysets, expected, { lang: 'lang' }); + }); + + it('must get keyset by lang', function () { + var keysets = { + 'block.i18n': { + 'ru.js': serialize({ + 'ru-scope': { + key: 'ru' + } + }), + 'en.js': serialize({ + 'en-scope': { + key: 'en' + } + }) + } + }, + expected = { + 'ru-scope': { + key: 'ru' + } + }; + + return assert(keysets, expected, { lang: 'ru' }); + }); + + it('must support function', function () { + var keysets = { + 'block.i18n': { + 'lang.js': serialize({ + scope: { + key: function () { return '^_^'; } + } + }) + } + }, + expected = { + scope: { + key: function () { return '^_^'; } + } + }; + + return assert(keysets, expected, { lang: 'lang' }); + }); + + describe('merge', function () { + it('must override value', function () { + var keysets = { + 'block-1.i18n': { + 'lang.js': serialize({ + scope: { + key: 'val' + } + }) + }, + 'block-2.i18n': { + 'lang.js': serialize({ + scope: { + key: 'val-2' + } + }) + } + }, + expected = { + scope: { + key: 'val-2' + } + }; + + return assert(keysets, expected, { lang: 'lang' }); + }); + + it('must merge keys', function () { + var keysets = { + 'block-1.i18n': { + 'lang.js': serialize({ + scope: { + 'key-1': 'val' + } + }) + }, + 'block-2.i18n': { + 'lang.js': serialize({ + scope: { + 'key-2': 'val' + } + }) + } + }, + expected = { + scope: { + 'key-1': 'val', + 'key-2': 'val' + } + }; + + return assert(keysets, expected, { lang: 'lang' }); + }); + + it('must merge scopes', function () { + var keysets = { + 'block-1.i18n': { + 'lang.js': serialize({ + 'scope-1': { + key: 'val-1' + } + }) + }, + 'block-2.i18n': { + 'lang.js': serialize({ + 'scope-2': { + key: 'val-2' + } + }) + } + }, + expected = { + 'scope-1': { + key: 'val-1' + }, + 'scope-2': { + key: 'val-2' + } + }; + + return assert(keysets, expected, { lang: 'lang' }); + }); + }); + }); + + describe('`i18n.js` files', function () { + it('must get empty keyset if empty lang file', function () { + var keysets = { + 'block.i18n.js': '' + }, + expected = {}; + + return assert(keysets, expected, { lang: 'lang' }); + }); + + it('must get keyset', function () { + var keysets = { + 'block.i18n.js': serialize({ + scope: { + key: 'val' + } + }) + }, + expected = { + scope: { + key: 'val' + } + }; + + return assert(keysets, expected, { lang: 'lang' }); + }); + + describe('merge', function () { + it('must override value', function () { + var keysets = { + 'block-1.i18n.js': serialize({ + scope: { + key: 'val' + } + }), + 'block-2.i18n.js': serialize({ + scope: { + key: 'val-2' + } + }) + }, + expected = { + scope: { + key: 'val-2' + } + }; + + return assert(keysets, expected, { lang: 'lang' }); + }); + + it('must merge keys', function () { + var keysets = { + 'block-1.i18n.js': serialize({ + scope: { + 'key-1': 'val' + } + }), + 'block-2.i18n.js': serialize({ + scope: { + 'key-2': 'val' + } + }) + }, + expected = { + scope: { + 'key-1': 'val', + 'key-2': 'val' + } + }; + + return assert(keysets, expected, { lang: 'lang' }); + }); + + it('must merge scopes', function () { + var keysets = { + 'block-1.i18n.js': serialize({ + 'scope-1': { + key: 'val-1' + } + }), + 'block-2.i18n.js': serialize({ + 'scope-2': { + key: 'val-2' + } + }) + }, + expected = { + 'scope-1': { + key: 'val-1' + }, + 'scope-2': { + key: 'val-2' + } + }; + + return assert(keysets, expected, { lang: 'lang' }); + }); + }); + }); + + describe('`i18n.js` files + `i18n` dirs', function () { + it('must add common scope', function () { + var keysets = { + 'block.i18n.js': serialize({ + 'common-scope': { + key: 'val' + } + }), + 'block.i18n': { + 'lang.js': serialize({ + 'lang-scope': { + key: 'val' + } + }) + } + }, + expected = { + 'common-scope': { + key: 'val' + }, + 'lang-scope': { + key: 'val' + } + }; + + return assert(keysets, expected, { lang: 'lang' }); + }); + + it('must override value', function () { + var keysets = { + 'block.i18n.js': serialize({ + scope: { + key: 'val' + } + }), + 'block.i18n': { + 'lang.js': serialize({ + scope: { + key: 'val-2' + } + }) + } + }, + expected = { + scope: { + key: 'val-2' + } + }; + + return assert(keysets, expected, { lang: 'lang' }); + }); + + it('must merge keys', function () { + var keysets = { + 'block.i18n': { + 'lang.js': serialize({ + scope: { + 'key-1': 'val' + } + }) + }, + 'block.i18n.js': serialize({ + scope: { + 'key-2': 'val' + } + }) + }, + expected = { + scope: { + 'key-1': 'val', + 'key-2': 'val' + } + }; + + return assert(keysets, expected, { lang: 'lang' }); + }); + + it('must merge scopes', function () { + var keysets = { + 'block.i18n': { + 'lang.js': serialize({ + 'scope-1': { + key: 'val-1' + } + }) + }, + 'block.i18n.js': serialize({ + 'scope-2': { + key: 'val-2' + } + }) + }, + expected = { + 'scope-1': { + key: 'val-1' + }, + 'scope-2': { + key: 'val-2' + } + }; + + return assert(keysets, expected, { lang: 'lang' }); + }); + }); + + describe('`i18n.js` files + `i18n` dirs with *.all.js files', function () { + it('must add common scope', function () { + var keysets = { + 'block.i18n.js': serialize({ + 'common-scope': { + key: 'val' + } + }), + 'block.i18n': { + 'all.js': serialize({ + 'lang-scope': { + 'key-1': 'val1' + } + }), + 'lang.js': serialize({ + 'lang-scope': { + 'key-2': 'val2' + } + }) + } + }, + expected = { + 'common-scope': { + key: 'val' + }, + 'lang-scope': { + 'key-1': 'val1', + 'key-2': 'val2' + } + }; + + return assert(keysets, expected, { lang: 'lang' }); + }); + + it('must override value by all.js file', function () { + var keysets = { + 'block.i18n.js': serialize({ + scope: { + key: 'val' + } + }), + 'block.i18n': { + 'all.js': serialize({ + scope: { + key: 'val-2' + } + }) + } + }, + expected = { + scope: { + key: 'val-2' + } + }; + + return assert(keysets, expected, { lang: 'lang' }); + }); + + it('must override value by lang.js file', function () { + var keysets = { + 'block.i18n.js': serialize({ + scope: { + key: 'val' + } + }), + 'block.i18n': { + 'lang.js': serialize({ + scope: { + key: 'val-3' + } + }), + 'all.js': serialize({ + scope: { + key: 'val-2' + } + }) + } + }, + expected = { + scope: { + key: 'val-3' + } + }; + + return assert(keysets, expected, { lang: 'lang' }); + }); + + it('must merge keys', function () { + var keysets = { + 'block.i18n': { + 'all.js': serialize({ + scope: { + 'key-all': 'val' + } + }), + 'lang.js': serialize({ + scope: { + 'key-1': 'val' + } + }) + }, + 'block.i18n.js': serialize({ + scope: { + 'key-2': 'val' + } + }) + }, + expected = { + scope: { + 'key-all': 'val', + 'key-1': 'val', + 'key-2': 'val' + } + }; + + return assert(keysets, expected, { lang: 'lang' }); + }); + + it('must merge scopes', function () { + var keysets = { + 'block.i18n': { + 'lang.js': serialize({ + 'scope-1': { + key: 'val-1' + } + }), + 'all.js': serialize({ + 'scope-all': { + key: 'val-all' + } + }) + }, + 'block.i18n.js': serialize({ + 'scope-2': { + key: 'val-2' + } + }) + }, + expected = { + 'scope-1': { + key: 'val-1' + }, + 'scope-all': { + key: 'val-all' + }, + 'scope-2': { + key: 'val-2' + } + }; + + return assert(keysets, expected, { lang: 'lang' }); + }); + }); + + it('must provide old core', function () { + var keysets = { + 'block.i18n': { + 'all.js': serialize({ + all: { + '': 'core' + } + }) + } + }, + expected = { + all: { + '': 'core' + } + }; + + return assert(keysets, expected, { lang: 'lang' }); + }); + + describe('cache', function () { + it('must get keysets from cache', function () { + var time = new Date(1); + + mock({ + blocks: { + 'block.i18n.js': mock.file({ + content: serialize({ scope: { key: 'val' } }), + mtime: time + }) + }, + bundle: {} + }); + + var bundle = new TestNode('bundle'), + dirname = path.resolve('blocks'), + fileList = new FileList(), + dirList = new FileList(); + + fileList.loadFromDirSync(dirname); + + bundle.provideTechData('?.files', fileList); + bundle.provideTechData('?.dirs', dirList); + + return bundle.runTechAndRequire(Tech, { lang: 'lang' }) + .spread(function () { + mock({ + blocks: { + 'block.i18n.js': mock.file({ + content: serialize({ scope: { key: 'val2' } }), + mtime: time + }) + }, + bundle: {} + }); + return bundle.runTechAndRequire(Tech, { lang: 'lang' }); + }) + .spread(function (res) { + res.must.eql({ scope: { key: 'val' } }); + }); + }); + + it('must ignore outdated cache', function () { + var time = new Date(1); + + mock({ + blocks: { + 'block.i18n.js': mock.file({ + content: serialize({ scope: { key: 'val' } }), + mtime: time + }) + }, + bundle: {} + }); + + var bundle = new TestNode('bundle'), + cache = bundle.getNodeCache('bundle.keysets.lang.js'), + dirname = path.resolve('blocks'), + basename = 'block.i18n.js', + filename = path.join(dirname, basename), + fileList = new FileList(), + dirList = new FileList(); + + fileList.loadFromDirSync(dirname); + + bundle.provideTechData('?.files', fileList); + bundle.provideTechData('?.dirs', dirList); + + dropRequireCache(require, filename); + require(filename); + cache.cacheFileInfo('keyset-file-' + basename, filename); + + mock({ + blocks: { + 'block.i18n.js': mock.file({ + content: serialize({ scope: { key: 'val2' } }), + mtime: new Date(2) + }) + }, + bundle: {} + }); + + return bundle.runTechAndRequire(Tech, { lang: 'lang' }) + .spread(function (res) { + res.must.eql({ scope: { key: 'val2' } }); + }); + }); + }); +}); + +function assert(keysets, expected, opts) { + opts || (opts = {}); + + var scheme = { + blocks: keysets, + bundle: {} + }; + + mock(scheme); + + var dirnames = Object.keys(keysets).filter(function (basename) { + return path.extname(basename) === '.i18n'; + }), + filenames = Object.keys(keysets).filter(function (basename) { + return path.extname(basename) === '.js'; + }), + bundle = new TestNode('bundle'), + fileList = new FileList(), + dirList = new FileList(), + root = 'blocks', + files = filenames.map(function (basename) { + var filename = path.resolve(root, basename); + + return FileList.getFileInfo(filename); + }), + dirs = dirnames.map(function (basename) { + var dirname = path.resolve(root, basename), + info = FileList.getFileInfo(dirname); + + info.files = fs.readdirSync(dirname).map(function (basename) { + var filename = path.join(dirname, basename); + + return FileList.getFileInfo(filename); + }); + + return info; + }); + + dirList.addFiles(dirs); + fileList.addFiles(files); + + bundle.provideTechData('?.files', fileList); + bundle.provideTechData('?.dirs', dirList); + + return bundle.runTechAndRequire(Tech, { lang: opts.lang }) + .spread(function (res) { + if (inspect(expected).indexOf('[Function]') === -1) { + // Если ожидается JSON-объект, то сравниваем как объекты + res.must.eql(expected); + } else { + // Если ожидается JavaScript, то сравниваем как строки + serialize(res).must.eql(serialize(expected)); + } + }); +} + +function serialize(js) { + return 'module.exports = ' + serializeJS(js) + ';'; +}