@yandex/ui
Version:
Yandex UI components
226 lines (181 loc) • 7.66 kB
JavaScript
const path = require('path');
const { renameJSXElement } = require('./utils/renameJSXElement');
const { isModulePath, normalizeImportPath } = require('./utils/importPath');
const { getJSXAttrValue, setJSXAttrValue } = require('./utils/JSXattribute');
const isPropChanged = (newVal, oldVal) => {
if (newVal === undefined) return false;
return newVal !== oldVal;
};
const handleName = (handlers = {}, block) => {
const { ComponentsPropsHandlers = {} } = handlers;
const nameHandler = ComponentsPropsHandlers[block] && ComponentsPropsHandlers[block].__name;
return nameHandler && nameHandler();
};
const handleProps = (handlers = {}, block, prop, nodePath) => {
const { CommonPropsHandlers = {}, ComponentsPropsHandlers = {} } = handlers;
const commonReplaced = CommonPropsHandlers[prop.name]
? CommonPropsHandlers[prop.name](prop.value, nodePath)
: undefined;
// немного нарокомании, писал поздно вечером
// Правильно мержим результат из Common и Components
const blockReplaced =
ComponentsPropsHandlers[block] && ComponentsPropsHandlers[block][prop.name]
? ComponentsPropsHandlers[block][prop.name](prop.value, nodePath)
: undefined;
const allReplaced = { ...commonReplaced, ...blockReplaced };
const changed =
isPropChanged(allReplaced.name, prop.name) ||
isPropChanged(allReplaced.value, prop.value) ||
allReplaced.removed;
return {
name: allReplaced.name || prop.name,
value: allReplaced.value || prop.value,
message: changed && allReplaced.shouldCheck,
removed: allReplaced.removed,
};
};
const processImports = (j, root, { from, to = from, componentName, newComponentName, componentsHandlers }) => {
const components = {};
let replaceImports = from !== to;
const Import = root.find(j.ImportDeclaration, {
source: {
value: from,
},
});
if (Import.size() === 0) {
return;
}
const specifiers = j(Import.get(0).node).find(j.ImportSpecifier);
const componentsToReplace = specifiers.filter(({ node }) => {
const blockName = node.imported.name;
if (componentName && componentName !== blockName) {
return false;
}
return true;
});
if (componentsToReplace.size() === 0) {
return;
}
let componentsToReplaceNames = [];
componentsToReplace.forEach(({ node }) => {
const imported = node.imported.name;
const local = node.local.name;
const newImported = newComponentName || handleName(componentsHandlers, imported) || imported;
// сохраняем локальные названия компонент, для последующей работы в JSX
components[local] = {
imported,
newImported,
newLocal: local === imported ? newImported : local,
};
if (replaceImports === false) {
replaceImports = newImported !== imported;
}
componentsToReplaceNames.push([local, imported]);
});
const newImport = j.importDeclaration(
componentsToReplaceNames.map(([local]) => {
const { newImported, newLocal } = components[local];
return j.importSpecifier(j.identifier(newImported), j.identifier(newLocal));
}),
j.stringLiteral(to),
);
// если меняем все компоненты из импорта
const fullImportReplace = specifiers.size() === componentsToReplace.size();
if (replaceImports) {
if (fullImportReplace) {
// меняем путь импортов на локальную обертку
Import.replaceWith(() => newImport);
} else if (from === to) {
specifiers.forEach(({ node }) => {
const local = node.local.name;
if (!components[local]) return;
const { newImported, newLocal } = components[local];
node.local.name = newLocal;
node.imported.name = newImported;
});
} else {
Import.at(0).insertAfter(newImport);
componentsToReplace.remove();
}
}
return components;
};
const processJSXComponents = (j, root, { componentsMap, componentsHandlers }) => {
return root.find(j.JSXOpeningElement).forEach((nodePath) => {
const node = nodePath.node;
const name = node.name;
let localName;
if (name) {
localName = name.name || name.object.name;
} else {
return;
}
if (!componentsMap[localName]) return;
const { imported: blockName } = componentsMap[localName];
j(node)
.find(j.JSXAttribute)
.forEach((nodePath) => {
const { node } = nodePath;
const name = node.name.name;
const value = getJSXAttrValue(node);
const { name: replacedName, value: replacedValue, removed, message } = handleProps(
componentsHandlers,
blockName,
{ name, value },
nodePath,
j,
);
if (message) {
nodePath.insertBefore(
`\/* TODO: lego-autoreplace - "${name}"(${removed ? 'удален' : 'изменён'}): ${message} *\/`,
);
}
if (removed) {
j(nodePath).remove();
return;
}
node.name.name = replacedName;
setJSXAttrValue(j, node, replacedValue);
});
const newLocalName = componentsMap[localName].newLocal;
const shouldNormalizeName = localName !== newLocalName;
if (shouldNormalizeName) {
renameJSXElement(nodePath.parent.node, newLocalName);
}
});
};
module.exports = function transformer(
file,
api,
{ import: from, newImport: to, specifier: componentName, newSpecifier: newComponentName, handler },
) {
// file.path отсуствует в тестах
if (!file.path) file.path = './test.js';
if (!from) {
throw Error('Укажите название путь импорта например lego-on-react или ./components/Button');
}
const j = api.jscodeshift;
const root = j(file.source);
const fromNormalized = normalizeImportPath(file.path, from);
const toNormalized = to && normalizeImportPath(file.path, to);
let componentsHandlers = {};
if (handler) {
const handlersPath = isModulePath(handler)
? path.join(__dirname, 'handlers', handler)
: path.resolve(handler);
componentsHandlers = require(handlersPath);
}
// меняем импорты и возвращаем мапинги имен для правильного учета названий локальных переменных
// нарушен принцип единственной отвественности, хорошо бы переделать
const componentsMap = processImports(j, root, {
from: fromNormalized,
to: toNormalized,
componentName,
newComponentName,
componentsHandlers,
});
if (componentsMap) {
processJSXComponents(j, root, { componentsMap, componentName, newComponentName, componentsHandlers });
}
return root.toSource({ quote: 'single' });
};