Open tadatuta opened 10 years ago
@SevInf:
По аналогии с LevelBuilder
:
Инстанс конструктора создается вызовоам BEM.defineTech()
. Непосредственно для создания класса используется метод createClass()
конструктора.
Базовый билдер:
setCreateSuffixes()
addCreateSuffixes()
setBuildSuffixes()
addBuildSuffixes()
setDependencies()
addDependencies()
buildChunk(callback)
build(callback)
create(callback)
setDefaultMakeMethod()
Расширение билдера делается методами самого билдера. Метод addParam()
автоматически генерирует методы для установки параметоров технологии (set<Param>()
и/или add<Param>()
).
Пара примеров:
Технология import-files
, собирающая выходной файл путем импортов из исходных:
module.exports = function(BEM) {
return BEM.defineTech()
.buildChunk(function(file) {
//этот метод станет getBuildResultChunk в классе технологии
return this.params.importTpl(file);
})
.addParam('importTpl', {
//сгенерирует метод setImportTpl у билдера import-files
set: function(value) {
//возвращаемое значение метода попадет в this.params. В данном случае
//это underscore- шаблон
return _.template(value);
}
})
.createClass();
};
На базе не можно делать технологию css:
module.exports = function(BEM) {
return BEM.defineTech('import-files') //задаем имя базовой технологии
.setBuildSuffixes('css')
.setImportTpl('@import url("<%=relPath %>")')
.createClass();
};
Или js-i:
module.exports = function(BEM) {
return BEM.defineTech('import-files')
.setBuildSuffixes('js')
.setImportTpl('/*borschik:include:<%=relPath %>')
.createClass();
};
Из которой в свою очередь можно сделать browser.js:
module.exports = function() {
return BEM.defineTech('js')
.addBuildSuffixes('js', ['vanilla.js', 'browser.js'])
.createClass();
};
@scf2k: Я бы оторвал set/add у названий методов чтобы короче было. @diunko: Если оторвать set/add, как их различать?
@tadatuta: я правильно понимаю, что Было:
exports.techMixin = {
getBuildResultChunk: function(relPath) {
return '/*borschik:include:' + relPath + '*/;\n';
},
getBuildSuffixesMap: function() {
return {
'js': ['js']
};
}
}
Стало:
module.exports = function(BEM) {
return BEM.defineTech('import-files')
.setBuildSuffixes('js')
.setImportTpl('/*borschik:include:<%=relPath %>')
.createClass();
};
и, грубо говоря, весь профит от TechBuilder
в возможности не писать function
и return
?
@scf2k: И в одном файле. @arikon: @tadatuta В конкретном случае — да. Но ещё упрощается процесс расширения существующих технологий (создаёшь объект-билдер из готовой технологии и зовёшь его хелперы), и технологии начинают предоставлять свои конкретные хелперы для конфигурирования себя.
@arikon
build(callback)
create(callback)
Куда применится переданный в эти методы callback
? Будет использован в качестве getCreateResult()
и getBuildResult()
?
@arikon:
Не очень понял про addParam(name, {})
.
Что можно передавать в качестве {}
, какие там могут быть ключи, помимо set
?
возвращаемое значение метода попадет в this.params
this.params
в контексте какого объекта?
@SevInf:
как расширить существующую технологию и добавить ей хелперов для билдера?
Если существующая технология написана с использованием билдера - то как-то так:
BEM.defineTech('existing-tech')
.addParam('extSuffix', {
set: function(value) {
this.addBuildSuffixes('ext.' + value, [value]);
}
});
Если речь про технологии, написание как через techMixin
- я не думаю, что это нужно. Если все-таки нужно - надо продумать, сейчас ответить не могу.
Куда применится переданный в эти методы callback? Будет использован в качестве getCreateResult() и getBuildResult()?
Да. С чего-то надо начинать строить самые базовые технологии.
Что можно передавать в качестве {}, какие там могут быть ключи, помимо set
По аналогии с LevelBuilder
думал сделать set
и/или add
this.params в контексте какого объекта?
Немного некорректно выразился. params
в итоговом классе технологии предлагаю сдеалать свойством прототипа или статическим свойством класса. Тогда инстанс сможет получить параметры через instance.params
или instance.__self.params
По аналогии с LevelBuilder думал сделать set и/или add
Сигнатура для add
:
add: function(prev, arg1, arg2...) {
}
Типичные варианты использования:
add: function(prev, value) {
prev = prev || [];
return prev.concat(value);
}
add: function(prev, key, value) {
prev = prev || {};
prev[key] = value;
return prev;
}
@arikon: Если у меня есть готовая технология, лежащая в пакете. Как мне расширить её?
Если речь про технологии, написание как через techMixin - я не думаю, что это нужно. Если все-таки нужно - надо продумать, сейчас ответить не могу.
Не думаю, что это нужно. Перепишем все технологии на использование билдера.
@SevInf:
Если у меня есть готовая технология, лежащая в пакете. Как мне расширить её?
Думаю, логично будет сделать по аналогии с LevelBuilder
:
var NewTech = OldTech.extend()
.someBuilderMethod()
.someOtherBuilderMethod()
.createClass();
extend
создает и возвращает точную копию билдера, используемого при создании OldTech
. Последующими вызовами его можно доконфигугрировать, не затрагивая OldTech
@SevInf: Чем больше погружаюсь в эту задачу, тем больше кажется что все проблемы которые мы хотим решить билдером можно в принципе решить более внятной иерархией классов технологий с правильными абстрактными классами и виртуальными методами в нужных местах. B в этом случае @tadatuta прав, профит от билдера будет не очень большим. По настоящему сделать API более гибким помог бы пересмотр flow базовой технологии, например в такую сторону:
create
как часть сборки уходит, остается один build
;build
принимает на вход декларацию, список уровней и опциональный префикс;buildFile(prefix, suffix, callback)
внутри каллбека происходит подбор исходных файлов и сборка выходного. Возвращает промис на строку/буфер которая попадет в выходной файл;getFile(prefix, suffix)
, getFilesFromLevels(levels, suffix)
и тому подобные. Эти методы могут вызваться толко внутри каллбека buildFiles
и в момент вызова происходит следующее:
mtime
сохраняются в кеше для текущего собираемого выходного файла;buildFile
с тем же префиксом и суффиксом все эти операции поднимаются из кеша, уровни повторно сканируются, новые списки файлов сравниваются с предыдущими и если они не изменились со времени прошлой сборки то вызов каллбека buildFiles
не происходит.Профит:
create
не вызывается в процессе сборки и не выносит людям мозгget<Something>SuffixesMap
Пара примеров (просьба воспринимать это скорей как псевдокод, чем реальное предложение API).
Вот как в этом случае может выглядеть гипотетический js-i
:
{
build: function(decl, levels, outputPrefix) {
this.buildFile(outputPrefix, 'js', function() {
return this.getFilesFromLevels(levels, ['js'])
.then(function(files) {
return files.map(function(file) {
return '/*borschik:include ' + file.relPath + '*/';
}).join('\n');
})
});
}
}
А вот так html
(попутно он сможет научиться собираться на нескольких уровнях, например desktop.blocks
и touch.blocks
и для нескольких бандлов сразу):
{
build: function(decl, levels) {
levels.forEach(function(level) {
decl.deps.forEach(function(item) {
this.buildFile(level.getPrefixFor(item), 'html', function() {
var bemhtml = this.getFile(level.getPrefixFor(item), 'bemhtml.js');
var bemjson = this.getFile(level.getPrefixFor(item), 'bemjson.js');
return bemhtml.apply(bemjson);
})
}, this);
}, this);
},
}
Для подобной базовой технологии билдер может все также иметь описанное выше API. Либо, новая базовая технология может изначально иметь builder-style API. @arikon, @tadatuta, @nar, нужен фидбек. Есть ли смысл в предложении в принципе, есть ли смысл в предложении в рамках bem-tools 1.0.0?
@arikon:
Сразу непонятно со сборкой html
и другими технологиями, которые сейчас выполняются как create()
.
Что в этом случае decl
и levels
?
Как будут выражаться и удовлетворяться зависимости для сборки html
сразу в нескольких бандлах и уровнях?
@narqo:
Кажется, я уже сильно выпал из контекста всего, что около внутренностей bem-tools. Я правильно понимаю, что технологии перестают быть завязаны на декларации в уровнях и становятся нормальными сборщиками? (по крайней мере везде используются суффиксы конечных файлов, а не названия технологий) — про что-то такое мы говорили в одном из PR на gh.
Еще не очень понимаю пример с html. Сейчас сборка html-файла, это: 1) прочитай bemjson, 2) прочитай bemhtml, 3) выполни bemhtml.apply(bemjson).
У тебя в примере получается, что файлы для this.getFile
будут искаться в уровнях пришедших из парамета levels
. Это немного странно (опять же, в текущем Мире, может что-то поменялось):
для сборки bemhtml
нужно будет передавать набор уровней с блоками (common.blocks
, desktop.blocks
, и т.д.),
для сборки html
— набор уровней (на самом деле один уровень?) с бандлами (desktop.bundles
).
За счет чего уйдет get*SuffixesMap
? Те же самые данные, ты сейчас хардкодишь в this.buildFile(outputPrefix, 'js', fn)
и this.getFilesFromLevels(levels, ['js'])
.
В остальном, мне эта идея нравится больше чем TechBuilder
, который я, пока, не понимаю :(
@SevInf:
Про html
:
Побочный эффект от такого способа объединения - одна create
-команда может собирать файлы на нескольких уровнях. Гипотетический вызов html
может выглядеть так:
levels=desktop.bundles, touch-phone.bundles;
decl
- список страниц которые надо собрать. Например [{block: "index"}, {block: "page1"}, ...]
В итоге соберется desktop.bundles/index/index.html, desktop.bundles/page1/page1.html, touch-phone.bundles/index/index.html, touch-phone.bundles/page1/page1.html
.
Если такая возможность считается бесполезной, то есть альтернативный вариант:build
принимает на вход только путь к текущему собираемому бандлу.create
- она продолжает работать также как и раньше - читает нужные исходники и пишет нужные выходные файлы.build
читает декларацию из бандла явно.build
-технологий нужна возможность получить список исходных уровней для бандла. Ныне покойный bundleBuildLevels
отлично бы для этого подошел.Я правильно понимаю, что технологии перестают быть завязаны на декларации в уровнях и становятся нормальными сборщиками?
По сути да. В предлагаемом варианте технология - это просто что-то что переводит набор входных файлов в набор выходных. При этом, как и откуда брать исходники решает сам автор модуля, никакого фиксированного flow в базовой технологии не предполагается.
За счет чего уйдет get*SuffixesMap? Те же самые данные, ты сейчас хардкодишь в this.buildFile(outputPrefix, 'js', fn) и this.getFilesFromLevels(levels, ['js']).
Я больше всякие штуки вроде weakSuffixes
имел ввиду. Ту же задачу можно было бы решить не созданием новой, довольно странной и неочевидной сущности в базовой технологии, а созданием альтернативного flow для суммарных технологий.
Данные от buildSuffixes
в том или ином виде все равно останутся, но пропадает необходимость их явно декларировать для того чтобы они попали в кеш - если для сборки нам надо прочитать что-то по нестандартному пути мы просто делаем это без всяких костылей.
Если такая возможность считается бесполезной, то есть альтернативный вариант:
На самом деле, этот альтернативный вариант теперь мне нравиться больше чем изначальное предложение.
Примеры псевдокода для тех же js-i
и html
:
{
build: function(bundleLevel) {
this.buildFile(bundleLevel.prefix, 'js', function() {
return this.readFile(bundleLevel.prefix, 'bemdecl.js')
.then(function(decl) {
return this.getFilesByDecl(decl, bundleLevel.getSourceLevels(), ['js']);
})
.then(function(files) {
return files.map(function(file) {
return '/*borschik:include ' + file.relPath + '*/';
}).join('\n');
});
});
}
}
{
build: function(bundleLevel) {
this.buildFile(bundleLevel.prefix, 'html', function() {
return this.readFile(bundleLevel.prefix, 'bemhtml.js')
.then(function(bemhtml) {
return [bemhtml, this.readFile(bundleLevel.prefix, 'bemjson.js')];
})
.spread(function(bemhtml, bemjson) {
return bemhtml.apply(bemjson);
});
})
},
}
Надо только договориться насчет bundleLevel.getSourceLevels()
.
В тред призываются @bem/owners и @zxqfox
Я, в целом, со согласен с @SevInf, и, как мне казалось, говорил про тоже самое. Ведь сами технологии — это плоский список трансформаций чего-то, описанный для уровня, а чаще и для всего проекта в целом, он будет знать что и из чего он собирает, и просто делать это.
Кроме этого, я еще хотел, но озвучивал по своему, вынести всяческие getFile куда-нибудь в bem-levels, или вообще отдельно, в bem-fs, может. Т.к. уровни у нас отвечают в т.ч. за именования в них — возможно, в нем эти getFile будут вполне ожидаемы. В любом случае, getFile, разбросанные по базовым уровням, технологиям, и т.д. — это зло. Реализации технологий (bemjson, bemhtml, deps) — это кирпичики, они всегда лежат в одинаковых местах и их можно провайдить одинаково в указанные технологии. Можно читать директории блоков и заранее узнавать про них, можно их даже читать. Опять же, если у нас будет 1 провайдер — у нас появится возможность из памяти провайдить как прочитанные с диска, так и собранные файлы реализаций технологий. Скажем, если мы говорим, что собираем html из bemjson, а bemjson у нас нет, но есть bemyaml, который может быть 1к1 трансформирован в bemjson технологией, которая заявила, что может это сделать — то мы делаем это, и сразу отдаем результат в технологию bemjson, а параллельно можем записать и bemjson, а можем и не писать — это становится не важно.
Есть еще мнение, что работать c getFile можно не напрямую из технологий, а из низкоуровневых модулей, типа bem-techs, может быть из базового класса технологии, да. А на высоком уровне, где мы будем описывать само преобразование lambda, у нас будет интерфейс аля Array или даже jQuery, которые позволят собирать все фильтры, мапы, редьюсы, лениво подгружать все нужное и отдавать в build самих технологий при необходимости. Т.е. если завернуть всю логику трансформаций в некую абстрактную коллекцию — сами лямбды станут легковесными и понятными, как раз для домохозяек. Скорость работы при этом не должна упасть, разве что на 5-10%, за счет создания небольшого кол-ва функций и их call'ов.
И последнее мнение — построение графа (deps) тоже можно описать технологией, обернуть в коллекцию, возможно даже с аналогичным интерфейсом, но несколько другой логикой внутри. Все таки с графом работаем.
В общем случае есть два вида преобразований: λt→t, λt→λ. Вторая, мне кажется, сводится к первой. Если вдаваться в подробности, то в одну λ может попадать разное кол-во входных данных, условно — λt[], λtw, λλt, которые тоже должны сводится к первой. Не суть.
Условно, технологии описывают преобразования: bemtree.js(bemtree[все кусочки со всех блоков]) → bemtree, если надо bemhtml.js(bemhtml[все кусочки со всех блоков]) → bemhtml, если надо [bemyaml(data.yaml) или bemtree(data.json) → bemjson], bemhtml(bemjson) → html Собрав все, а это можно сделать при старте прочитав make и levels, это подграф из всех известных исходников и целей. Узлы — реализации или данные (условно, term), хорды — преобразования (условно, lambda). Поскольку могут быть случаи, когда возможно собрать из разных источников — жедательно где-то указывать порядок, либо же брать из порядка декларации/подключения технологий.
build: function (bundleLevel) {
// тут стоит оставить html, bundleLevel.prefix — лучше убрать за интерфейс bem-fs или bem-levels
this.buildFile(bundleLevel.prefix, 'html', function() {
// prefix туда же, по той же причине
// про bemhtml.js должна знать зависимая от html лямбда
return this.readFile(bundleLevel.prefix, 'bemhtml.js')
.then(function(bemhtml) {
// через которую мы узнаем и про bemjson.js
return [bemhtml, this.readFile(bundleLevel.prefix, 'bemjson.js')];
})
.spread(function(bemhtml, bemjson) {
// и тогда только это у нас останется в then колбеке в каком-то виде
// и только потому, что нам надо λ(λt)→t
// подозреваю, что и это можно будет описать
return bemhtml.apply(bemjson);
});
})
}
BEM.defineTech('existing-tech')
.addParam('extSuffix', {
может быть лучше так?
BEM.defineTech('existing-tech', function (existingTech) {
existingTech.defineProperty('extSuffix', {
set: ...
});
});
Или вообще через provide, как в ym
.
@zxqfox, правильно ли я понял, что технология js
в твоем предложении будет выглядет как то так:
//allJs - все JS-файлы, собранные из блоков, array-like интерфейс.
var builtJS = allJs.map(function(file) {
return '/*borscik:include ' + file.path + '*/';
}).reduce(function(file1, file2) {
return file1 + file2;
});
При этом, сам allJS
- продукт работы другой технологии. Сам builtJS
в последствии может быть использован как исходник для какой-то другой технологии, например js+bemhtml
?
@SevInf ± да. условно:
var allJs = all.reduce(function (node, result) {
node.techs['vanilla.js'] && result.push(node.techs['vanilla.js'].name());
return result;
}, []);
@SevInf Конечно же, частоиспользуемые вещи надо будет засахарить. reduce
слегка сносит мозг. Да и закешировать тоже или предоставить интерфейс через какой-то модуль, например, тот же bem-core, который предоставляет сами технологии.
Можно представить all как DB про БЭМ-объекты. Например, блоки — записи, технологии — поля, но могут быть представлены как записи, библиотеки — таблицы. Грубо, конечно, но близко к истине.
В таком случае многое вообще становится не нужно, достаточно будет предоставить простой интерфейс (взять, положить, и т.д.) и описать язык взаимодействия.
@zxqfox, да, кажется что это движение в правильном направлении. Сами сборщики на низком уровне тогда очень просто можно было бы описывать декларативно, a-la Rake/Jake/модульная система:
BEM.builder('allJs', ['all'], function(all) {
return all.reduce(...);
});
BEM.builder('js', ['allJs'], function(allJs) {
return allJs.map(...)
});
BEM.builder('js+bemhtml', ['js', 'bemhtml.js'], function(js, bemhtml) {
return js.concat(bemhtml);
});
@SevInf :+1: Да, это именно то, о чем я думаю
Кстати, заодно и хороший повод термин "технология" заменить на что-нибудь более очевидное, например "task" или "build step":)
Название надо придумать, да. Task
ок в совокупности с builder
.
Может быть возможность асинхронно провайдить тоже нужна? Какой тогда интерфейс лучше? UPD: promises.
BEM.builder('js+bemhtml', ['js', 'bemhtml.js'], function(js, bemhtml) {
var promise = some.promise();
setTimeout(function () {
promise.fulfill(js.concat(bemhtml));
}, 500);
return promise;
});
Кажется, для асинхронности самым правильным, удобным и легкочитаемым будет возврат промиса.
@SevInf Да, согласен. Все время про них забываю. :aerial_tramway:
@SevInf А почему bem.builder
?
@zxqfox, никакой конретной причины нет, просто чтобы псевдокод продемонстрировать. Если остановились на том, что это таск то наверное правильнее будет bem.task()
@SevInf завтра же сменю ник на @zqfox ;-)
Только ведь в таком виде task
не сходится с командами, которые выбрасываются в BEM.api
и в coa
.
И еще, ноды полезны для deps
, mergedBundles
, еще для чего-то? Или merged
как-то проще можно будет собрать?
Хотя, да, конечно можно будет проще:
all.filter({ merged }).reduce( ... )
завтра же сменю ник на @zqfox ;-)
да ладно, я учусь потихоньку)
С командами, возможно, действительно не очень сходится. Есть пока слабо оформившаяся идея, сделать возможность объявлять некоторые таски вызываемыми как комманды:
BEM.task('make', ['js', 'css', 'html'], function(deps, cliOpts) {
//что-то делаем с опциями. что?
})
.command()
.option('someOpt').short('o').long('opt')
.arg('someArg')
Таким образом make
можно будет вызвать из командной строки, передать ей опции и аргументы. Для того чтобы позвать make
нам надо собрать, js
, css
и html
. Для js
в свою очередь нужен allJs
. Для alJsl
нужен all
и так далее.
Но идея очень сырая, и я не уверен что это мысль в правильном направлении. Пока такая штука больше вопросов вызывает, чем дает ответов:
В общем, хотелось бы подтверждения или опровержения негодности этой идеи.
@SevInf что сейчас, что раньше — с трудом представляю как можно такие штуки из терминала использовать.
Но, с другой стороны exec [-l bundle || cwd()] <task>
может в stdout
выплевывать результат работы, если это 1 файл. Иначе непонятно, как форматировать. Либо же еще -o file
или -o path
, чтобы оно туда все сохранило.
@SevInf т.е. не напрямую в коа, а через прослойку, которая специально для этих трансформаций сделана.
Кстати, по смыслу это преобразование множеств, искал какое-то слово правильное, но кроме лямбды ничего в голову не пришло. Можно transformRule
, или rule
, а task
оставить для команд, почему-то там мне оно ощущается более гармонично, чем в переходах, ближе к командной строке.
Вообще, теория множеств и графы — это то, чем можно описать БЭМ, почти полностью. Если есть сложности с именованием — лучше обращаться к первоисточникам ;-), и классикам.
@SevInf @zxqfox Из треда не понял, что будет делать технологии all
и allJS
. Расскажите?
Лёша, ты уже начал где-то делать прототип?
@SevInf @zxqfox Если я правильно распарсил интерфейс билдера, то первым аргументом указывается имя описываемого билдера (технологии, таска), а вторым — его зависимости (другие билдеры / технологии / таски).
Статические зависимости сейчас в bem make
, и я склоняюсь к мысли, что это скорее не очень удобно, чем наоборот. Какие ваши мысли на этот счёт?
@arikon Первый — то, что описываем, второй — что подтягиваем. Как в AMD. all и allJs — это все доступные для сборки технологии (реализации) и только Js-based соотв. Есть одна большая коллекция, мы её фильтруем/преобразовываем, получаем новые коллекции, технологии и, в итоге, результат. Но делаем мы это с конца, чтобы не собирать лишнее. Читаем декларации, вычисляем по дереву зависимостей что требуется, и оно само собирается при необходимости. Про статические зависимости — честно говоря, не понял о чем речь. Думаю, что в идеале такого быть не должно в принципе, потому что не нужно. Но там где нужно — возможность их использовать, думаю, есть. Ответил настолько же точно, насколько понял вопрос ;-)
Лёша, ты уже начал где-то делать прототип?
@arikon Да, конечно, но бОльшая часть пока в голове, будет стыдно, если станет видно. Уже надо? Есть же 1.0, если форсировать разработку 2.0, мы 1.0 перепрыгнем, оно же в альфе до сих пор.
@zxqfox Чем раньше от слов к делу, тем лучше ;) 1.0 ждёт своего героя — есть ещё пачка неокученных задач, которые обещал сделать @diunko.
@arikon Понял. У меня эта неделя слегка загруженная, но к концу недели я постараюсь формализовать и реализовать все то, что уже сформировалось. База почти вся ясна. Есть некоторые хотелки типа поддержки старых технологий, и с ними пока туман.
@zxqfox Я бы в 2.0 вообще не тащил никакого legacy
@arikon Даже через отдельный модуль-адаптер? ;-)
@zxqfox Как миниму, я бы не держал это в фокусе сейчас. Поиск решения — как это сделать — однозначно сужает спектр возможных технических решений для bem-tools@2.0
.
@arikon Сергей, я тебя прекрасно понял ;-) И согласен.
Здесь собираем варианты API TechBuilder, выбираем наиболее подходящий.