Йомен помогал командам создавать проекты и модули быстрее и удобнее, чем когда-либо. Добавление новых файлов в проект стало до глупости простым. Файлы обрабатывались как шаблоны, наполнялись данными и копировались. Однако с обновлением файлов была немного другая история.
Когда был выпущен Yeoman, файлы JavaScript, которые нужно было изменить, рассматривались как файлы. Часто люди добавляли маркеры типа /* ADD IMPORT */
, которые затем сопоставлялись с регулярными выражениями и заменялись строкой. Если вы когда-либо посещали занятия по теоретической информатике, вы также можете помнить, что анализ программы с помощью RegEx ненадежен. Последняя документация Yeoman советует вам анализировать AST файла и изменять его, а не анализировать его с помощью RegEx.
Разбор файла кода с помощью RegEx - опасный путь, и перед этим вам следует прочитать Антропологические ответы этой CS и понять недостатки парсинга RegEx.
Ответ StackOverflow - классический, и его определенно стоит прочитать. В документации также указано следующее:
Обновление уже существующего файла - не всегда простая задача. Самый надежный способ сделать это - проанализировать файл AST (абстрактное синтаксическое дерево) и отредактировать его. Основная проблема с этим решением заключается в том, что редактирование AST может быть многословным и трудным для понимания.
Соревнование
Хотя в документации четко указано, что нам следует делать для изменения файлов, они не объясняют, как можно анализировать и изменять AST.
Недавно я работал над небольшим генератором для проекта React + Redux. Один из подгенераторов должен добавить action
, reducer
и импортировать созданный редуктор в корневой редуктор, который выглядит следующим образом:
import { combineReducers } from ‘redux’; import examplesReducer from ‘./reducers/examples’; export default combineReducer({ examples: examplesReducer, });
В этой статье я сосредоточусь на том, как автоматически import
вновь созданный редуктор, но добавление его к аргументу, предоставленному для combineReducer
, следует тому же подходу.
Из текста в AST
Для работы с JavaScript AST у вас есть несколько вариантов, три из которых довольно популярны: Babel, TypeScript и Esprima. Их сравнение - отдельная тема, выходящая за рамки данной статьи. Из-за моего знакомства с Babel и некоторыми изящными библиотеками вокруг него, особенно в отношении обхода AST, я решил пойти с ним.
Парсить AST из файла на самом деле довольно просто:
const babylon = require('babylon'); const source = this.fs.read( this.destionationPath('src/reducers.js') ); const ast = babylon.parse(source, { sourceType: 'module', });
(Очень!) Упрощенная версия AST приведенного выше файла-редуктора будет выглядеть так:
[ { "type":"ImportDeclaration", "specifiers":[ { "type":"ImportSpecifier", "imported":{ "name":"combineReducers" }, "source":{ "type":"StringLiteral", "value":"redux" } } ] }, { "type":"VariableDeclaration", "declarations":[ { "type":"VariableDeclarator", "id":{ "type":"Identifier", "name":"rootReducer" }, "init":{ "type":"CallExpression", "callee":{ "type":"Identifier", "name":"combineReducers", "arguments":[...] } } } ], "kind":"const" } ]
Эти два узла примерно представляют эти две линии:
import { combineReducers } from 'redux'; const rootReducer = combineReducers(/* ... */);
Каждый фрагмент кода представлен в AST. Чтобы изменить наш код, нам нужно пройти по дереву и найти правильный узел, который мы хотим изменить.
Добавление декларации об импорте
Поскольку мы уже импортируем из пакета redux
, но не знаем, сколько редукторов уже импортируется, мы хотим добавить новое объявление после последнего.
Пакет babel-traverse
помогает нам пройти AST, а babel-types
предоставляет помощников для идентификации определенных токенов. Хороший способ путешествовать по деревьям - это шаблон дизайна посетителей, который babel-traverse
использует.
const traverse = require('babel-traverse').default; let lastImport = null; traverse(ast, { ImportDeclaration(path) { lastImport = path; } });
Добавление нового объявления импорта после последнего означает, что его следует добавить, когда следующий токен не является другим импортом. К сожалению, шаблон не дает нам возможности заглянуть вперед в дереве и увидеть, каким будет следующий узел. Поэтому нам нужно запомнить последнее объявление импорта.
Если мы позволим этому выполниться и проверим lastImport
, вы увидите, что на самом деле это последнее объявление импорта. Если мы посещаем узел, мы можем проверить, был ли определен lastImport
и не является ли текущий посещаемый узел еще одним объявлением импорта.
traverse(ast, { enter(path) { if (lastImport && !isImportDeclaration(path)) { // Add the new import declaration } } });
В реальном AST объявление импорта - это не просто один узел, поэтому isImportDeclaration
немного сложнее.
const t = require('babel-types'); const isImportDeclaration = path => t.isImportDeclaration(path.node) || t.isImportSpecifier(path.parent) || t.isImportDeclaration(path.parent) || t.isImportSpecifier(path.parent) || t.isImportDefaultSpecifier(path.parent);
На данный момент мы знаем, 1) что текущий узел не является еще одним объявлением импорта и 2) последним объявлением импорта. С этой информацией мы можем вставить новое объявление.
const declaration = t.importDeclaration( [t.importDefaultSpecifier(t.identifier('tasksReducer'))], t.stringLiteral('./reducers/tasks'), );
t.importDeclaration
назначает новый узел declaration
, который будет разрешен при генерации кода для import tasksReducer from './reducers/tasks';
. К счастью, добавить declaration
в AST довольно просто.
traverse(ast, { enter(path) { if (lastImport && !isImportDeclaration(path)) { lastImport.insertAfter(declaration); } }, ImportDeclaration(path) { lastImport = path; } });
🎉 Поздравляю, тяжелая работа сделана! Теперь вам нужно только снова преобразовать AST в код, что невероятно просто с babel-generator
.
const generate = require('babel-generator').default; const { code } = generate(ast, { /* config */ }, source); this.fs.write(this.destinationPath('src/reducers.js', code);
Вот и все! Конечно, этот шаблон можно адаптировать и к другим модификациям AST. Например, вы можете автоматически сопоставить ключ состояния с его редуктором, реализовав ObjectProperty
посетителя. Но честно говоря, имея доступ к AST, вы можете делать все, что захотите.
Надеюсь, эта статья помогла вам правильно изменить файлы JavaScript с помощью Yeoman. Это может быть немного сложно для декларации импорта, но весело поиграть с AST Babel и предложить вам некоторое представление о языке.
Для записи вот полный код, используемый в генераторе: