UNPKG

babel-plugin-glaze

Version:

Babel plugin to transform sx prop

260 lines (211 loc) 9.28 kB
"use strict"; var _core = require("@babel/core"); /* eslint-disable @typescript-eslint/explicit-function-return-type, no-param-reassign */ /* Source: https://github.com/facebook/react/blob/master/packages/eslint-plugin-react-hooks/src/RulesOfHooks.js */ function isComponentName(name) { return !/^[a-z]/.test(name); } /* Source: https://github.com/facebook/react/blob/master/packages/eslint-plugin-react-hooks/src/RulesOfHooks.js */ function isHookName(name) { return /^use[A-Z0-9].*$/.test(name); } /* Source: https://github.com/facebook/react/blob/master/packages/eslint-plugin-react-hooks/src/RulesOfHooks.js */ function isComponent(node) { return _core.types.isIdentifier(node) && isComponentName(node.name); } /* Source: https://github.com/facebook/react/blob/master/packages/eslint-plugin-react-hooks/src/RulesOfHooks.js */ function isHook(node) { if (_core.types.isIdentifier(node)) { return isHookName(node.name); } if (_core.types.isMemberExpression(node) && !node.computed && _core.types.isIdentifier(node.property) && isHook(node.property)) { return _core.types.isIdentifier(node.object) && node.object.name === 'React'; } return false; } /* Source: https://github.com/facebook/react/blob/master/packages/eslint-plugin-react-hooks/src/RulesOfHooks.js#L542 */ function getFunctionIdentifier(path) { const { node } = path; const { parent } = path; if (path.isFunctionDeclaration()) { return path.node.id || undefined; } if (path.isFunctionExpression()) { return path.node.id || undefined; } if (path.isFunctionExpression() || path.isArrowFunctionExpression()) { if (_core.types.isVariableDeclarator(parent) && parent.init === node && _core.types.isIdentifier(parent.id)) { return parent.id; } if (_core.types.isAssignmentExpression(parent) && parent.right === node && parent.operator === '=' && _core.types.isIdentifier(parent.left)) { return parent.left; } if (_core.types.isProperty(parent) && parent.value === node && _core.types.isIdentifier(parent.key)) { return parent.key; } if (_core.types.isAssignmentPattern(parent) && parent.right === node && _core.types.isIdentifier(parent.left)) { return parent.left; } } else { return undefined; } return undefined; } // eslint-disable-next-line @typescript-eslint/no-explicit-any function findNearestParentComponent(path) { while (path) { const { scope } = path; const node = path.scope.path; if (_core.types.isExportDefaultDeclaration(scope.path.parent)) { return node; } const functionIdentifier = getFunctionIdentifier(scope.path); if (functionIdentifier) { if (isComponent(functionIdentifier) || isHook(functionIdentifier)) { return node; } } path = scope.path.parentPath; } return undefined; } module.exports = function plugin({ types: t }) { return { name: 'babel-plugin-glaze', visitor: { Program: { enter(path, state) { state.hasSxProps = false; state.pathsToAddHook = new Set(); }, exit(path, { hasSxProps }) { if (hasSxProps) { const importUseStyling = _core.template.ast`import { useStyling } from 'glaze'`; const { node } = path; // Check if something from glaze is already imported const glazeImportDeclaration = node.body.find(s => t.isImportDeclaration(s) && s.source.value === 'glaze'); // Something is already imported from glaze if (t.isImportDeclaration(glazeImportDeclaration)) { // Check if it's `useStyling` which is imported const useStylingImport = glazeImportDeclaration.specifiers.find(s => t.isImportSpecifier(s) && s.imported.name === 'useStyling'); // If it's not `useStyling`, we add it to the import if (!useStylingImport) { glazeImportDeclaration.specifiers.push(t.importSpecifier(t.identifier('useStyling'), t.identifier('useStyling'))); } } // Nothing imported yet from glaze else { path.unshiftContainer('body', importUseStyling); } } } }, Function: { exit(path, state) { if (state.pathsToAddHook.has(path)) { const nodeToAddHook = path.node; const createUseStylingHook = _core.template.statement.ast` const sx = useStyling(); `; if (t.isBlockStatement(nodeToAddHook.body)) { /* Verify that the hook is not yet created. If not,we create it */ const block = nodeToAddHook.body; const isAlreadyImported = block.body.some(st => t.isVariableDeclaration(st) && st.declarations.find(decl => t.isCallExpression(decl.init) && t.isIdentifier(decl.init.callee, { name: 'useStyling' }))); if (!isAlreadyImported) { nodeToAddHook.body.body.unshift(createUseStylingHook); } } else { /* Not a block statement. We first need to create one. Example: const Comp = () => <div sx={{color: "blue"}}>hello</div> Should become: const Comp = () => { return <div sx={{color: "blue"}}>hello</div> } */ nodeToAddHook.body = t.blockStatement([createUseStylingHook, t.returnStatement(nodeToAddHook.body)]); } } } }, JSXAttribute(path, state) { const { node } = path; if (t.isJSXIdentifier(node.name, { name: 'sx' })) { const jsxIdentifier = node.name; if (t.isJSXExpressionContainer(node.value)) { if (t.isExpression(node.value.expression)) { /* 1. We set this value so we know that somewhere in the file the `sx` props is used.We will need therefore to import `useStyling` hook from 'glaze'. This is done in the Program exit. */ state.hasSxProps = true; /* 2. Find the nearest parent component (or the current scope) and add it to a list that will be processed wihtin the Function exit. This is where we will create de `sx` variable: `const sx = useStyling()` */ const pathsToAddHook = findNearestParentComponent(path) || path.scope.path; if (pathsToAddHook.isFunction()) { state.pathsToAddHook.add(pathsToAddHook); } /* 3. This is where we transform the `sx` props */ if (t.isJSXOpeningElement(path.parent)) { const objectExpression = node.value.expression; // Remove the `sx` props path.remove(); // Check if a className props already exists let classNameAttributeIdentifier = path.parent.attributes.find(a => t.isJSXAttribute(a) && t.isJSXIdentifier(a.name, { name: 'className' })); if (t.isJSXAttribute(classNameAttributeIdentifier)) { // A className props already exists const classNameNode = classNameAttributeIdentifier.value; const baseTemplateLiteral = t.templateLiteral([t.templateElement({ raw: '' }, false), t.templateElement({ raw: '' }, true)], [t.callExpression(t.identifier(jsxIdentifier.name), [objectExpression])]); /* Handle the case where className is currently a an expression. E.g: `className={fn(...)}` or `className={isValid ? "my-class" : ""}` */ if (t.isJSXExpressionContainer(classNameNode) && t.isExpression(classNameNode.expression)) { baseTemplateLiteral.quasis.splice(1, 0, t.templateElement({ raw: ' ' }, false)); baseTemplateLiteral.expressions.unshift(classNameNode.expression); } else if (t.isStringLiteral(classNameNode)) { /* Handle the case where the className is currently a string */ if (classNameNode.value !== '') { baseTemplateLiteral.quasis[0] = t.templateElement({ raw: `${classNameNode.value} ` }, false); } } classNameAttributeIdentifier.value = t.jsxExpressionContainer(baseTemplateLiteral); } else { /* Handle the case where no className exists yet */ classNameAttributeIdentifier = t.jsxAttribute(t.jsxIdentifier('className'), t.jsxExpressionContainer(t.callExpression(t.identifier(jsxIdentifier.name), [objectExpression]))); path.parent.attributes.unshift(classNameAttributeIdentifier); } } } } } } } }; }; //# sourceMappingURL=index.js.map