UNPKG

@yandex/ui

Version:

Yandex UI components

226 lines (181 loc) 7.66 kB
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' }); };