UNPKG

babel-plugin-styled-components

Version:

Improve the debugging experience and add server-side rendering support to styled-components

227 lines (219 loc) 11.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _helperModuleImports = require("@babel/helper-module-imports"); var _detectors = require("../utils/detectors"); var _options = require("../utils/options"); var _process = require("./process"); // Most of this code was taken from @satya164's babel-plugin-css-prop const TAG_NAME_REGEXP = /^[a-z][a-z\d]*(\-[a-z][a-z\d]*)?$/; const ISSUE_URL = 'https://github.com/styled-components/babel-plugin-styled-components'; const getName = (node, t, path) => { if (typeof node.name === 'string') return node.name; if (t.isJSXMemberExpression(node)) { return `${getName(node.object, t, path)}.${node.property.name}`; } throw path.buildCodeFrameError(`Cannot infer name from node with type "${node.type}". Please submit an issue at ${ISSUE_URL} with your code so we can take a look at your use case!`); }; const getNameExpression = (node, t, path) => { if (typeof node.name === 'string') return t.identifier(node.name); if (t.isJSXMemberExpression(node)) { return t.memberExpression(getNameExpression(node.object, t, path), t.identifier(node.property.name)); } throw path.buildCodeFrameError(`Cannot infer name expression from node with type "${node.type}". Please submit an issue at ${ISSUE_URL} with your code so we can take a look at your use case!`); }; const getLocalIdentifier = path => { const identifier = path.scope.generateUidIdentifier('css'); // make it transient identifier.name = identifier.name.replace('_', '$_'); return identifier; }; const PROGRAM_BODY_PRENUMBERED = 'styled-components-program-prenumbered'; var _default = t => { const taggedTemplateVisit = (0, _process.processTaggedTemplate)(t); const callExpressionVisit = (0, _process.processCallExpression)(t); return (path, state) => { if (!(0, _options.useCssProp)(state)) return; if (path.node.name.name !== 'css') return; const program = state.file.path; // state.customImportName is passed through from styled-components/macro if it's used // since the macro also inserts the import let importName = state.customImportName || (0, _detectors.importLocalName)('default', state); const { bindings } = program.scope; const importBindingName = importName && (importName.name || importName); const importBinding = importBindingName && bindings[importBindingName]; const importBindingIsNamespace = importBinding && importBinding.path && importBinding.path.isImportNamespaceSpecifier(); // A namespace binding (`import * as styled from 'styled-components'`) is // not directly callable, so treat it the same as "no default binding" and // inject a fresh default import to use as the css-prop callee. if (!importBinding || importBindingIsNamespace) { (0, _helperModuleImports.addDefault)(path, (0, _options.useCssPropImportPath)(state), { nameHint: 'styled' }); importName = t.identifier((0, _detectors.importLocalName)('default', state, { bypassCache: true })); } if (!t.isIdentifier(importName)) importName = t.identifier(importName); const elem = path.parentPath; const name = getName(elem.node.name, t, path); const nameExpression = getNameExpression(elem.node.name, t, path); const id = path.scope.generateUidIdentifier('Styled' + name.replace(/^([a-z])/, (match, p1) => p1.toUpperCase())); let styled; let injector; if (TAG_NAME_REGEXP.test(name)) { styled = t.callExpression(importName, [t.stringLiteral(name)]); } else { styled = t.callExpression(importName, [nameExpression]); if (bindings[name] && !t.isImportDeclaration(bindings[name].path.parent)) { injector = nodeToInsert => (t.isVariableDeclaration(bindings[name].path.parent) ? bindings[name].path.parentPath : bindings[name].path).insertAfter(nodeToInsert); } } let css; if (t.isStringLiteral(path.node.value)) { css = t.templateLiteral([t.templateElement({ raw: path.node.value.value, cooked: path.node.value.value }, true)], []); } else if (t.isJSXExpressionContainer(path.node.value)) { if (t.isTemplateLiteral(path.node.value.expression)) { css = path.node.value.expression; } else if (t.isTaggedTemplateExpression(path.node.value.expression) && path.node.value.expression.tag.name === 'css') { css = path.node.value.expression.quasi; } else if (t.isObjectExpression(path.node.value.expression)) { css = path.node.value.expression; } else { css = t.templateLiteral([t.templateElement({ raw: '', cooked: '' }, false), t.templateElement({ raw: '', cooked: '' }, true)], [path.node.value.expression]); } } if (!css) return; // strip off css prop from final output elem.node.attributes = elem.node.attributes.filter(x => t.isJSXSpreadAttribute(x) || (t.isJSXAttribute(x) ? x.name.name !== 'css' : false)); elem.node.name = t.jSXIdentifier(id.name); if (elem.parentPath.node.closingElement) { elem.parentPath.node.closingElement.name = t.jSXIdentifier(id.name); } // object syntax if (t.isObjectExpression(css)) { /** * for objects as CSS props, we have to recurse through the object and replace any * object key/value scope references with generated props similar to how the template * literal transform above creates dynamic interpolations */ const p = t.identifier('p'); let replaceObjectWithPropFunction = false; css.properties = css.properties.reduce(function propertiesReducer(acc, property) { /** * handle potential object key interpolations */ if (t.isMemberExpression(property.key) || t.isCallExpression(property.key) || // checking for css={{[something]: something}}; a plain (non-computed) // identifier key is a literal property name and never resolves to a // binding, even if the local scope happens to define a same-named // variable. Only computed keys reference the surrounding scope. t.isIdentifier(property.key) && property.computed && path.scope.hasBinding(property.key.name) && ( // but not a object reference shorthand like css={{ color }} t.isIdentifier(property.value) ? property.key.name !== property.value.name : true) && // and not a tricky expression !t.isMemberExpression(property.value) && !t.isLogicalExpression(property.value) && !t.isConditionalExpression(property.value)) { replaceObjectWithPropFunction = true; const identifier = getLocalIdentifier(path); elem.node.attributes.push(t.jSXAttribute(t.jSXIdentifier(identifier.name), t.jSXExpressionContainer(property.key))); property.key = t.memberExpression(p, identifier); } if (t.isObjectExpression(property.value)) { // recurse for objects within objects (e.g. {'::before': { content: x }}) property.value.properties = property.value.properties.reduce(propertiesReducer, []); acc.push(property); } else if (t.isSpreadElement(property)) { // handle spread variables and such if (t.isObjectExpression(property.argument)) { property.argument.properties = property.argument.properties.reduce(propertiesReducer, []); } else { replaceObjectWithPropFunction = true; const identifier = getLocalIdentifier(path); elem.node.attributes.push(t.jSXAttribute(t.jSXIdentifier(identifier.name), t.jSXExpressionContainer(property.argument))); property.argument = t.memberExpression(p, identifier); } acc.push(property); } else if ( // if a non-primitive value we have to interpolate it [t.isBigIntLiteral, t.isBooleanLiteral, t.isNullLiteral, t.isNumericLiteral, t.isStringLiteral].filter(Boolean) // older versions of babel might not have bigint support baked in .every(x => !x(property.value))) { replaceObjectWithPropFunction = true; const identifier = getLocalIdentifier(path); elem.node.attributes.push(t.jSXAttribute(t.jSXIdentifier(identifier.name), t.jSXExpressionContainer(property.value))); acc.push(t.objectProperty(property.key, t.memberExpression(p, identifier), property.computed, // shorthand requires `value` to be the same Identifier as `key`; // after rewriting `{ color }` to `{ color: p.$_cssN }` the // invariant no longer holds, so force-clear the flag. false)); } else { // some sort of primitive which is safe to pass through as-is acc.push(property); } return acc; }, []); if (replaceObjectWithPropFunction) { css = t.arrowFunctionExpression([p], css); } } else { // tagged template literal css.expressions = css.expressions.reduce((acc, ex) => { if (Object.keys(bindings).some(key => bindings[key].referencePaths.find(p => p.node === ex)) || t.isFunctionExpression(ex) || t.isArrowFunctionExpression(ex)) { acc.push(ex); } else { const identifier = getLocalIdentifier(path); const p = t.identifier('p'); elem.node.attributes.push(t.jSXAttribute(t.jSXIdentifier(identifier.name), t.jSXExpressionContainer(ex))); acc.push(t.arrowFunctionExpression([p], t.memberExpression(p, identifier))); } return acc; }, []); } if (!injector) { let parent = elem; while (!t.isProgram(parent)) { parent = parent.parentPath; } injector = nodeToInsert => parent.pushContainer('body', nodeToInsert); } // Eagerly number every user-written styled component already present in // Program.body BEFORE the css-prop injection lands, then number the // injected declaration via the explicit loop below. Babel's outer // traversal does not reliably re-visit nodes pushed onto Program from // inside Program-enter, so without this pre-pass user-written components // discovered later would compete with injected ones for counter values. if (!state.file.get(PROGRAM_BODY_PRENUMBERED)) { state.file.set(PROGRAM_BODY_PRENUMBERED, true); program.traverse({ TaggedTemplateExpression(p) { taggedTemplateVisit(p, state); }, CallExpression(p) { callExpressionVisit(p, state); } }); } const insertedPaths = injector(t.variableDeclaration('var', [t.variableDeclarator(id, t.isObjectExpression(css) || t.isArrowFunctionExpression(css) ? t.callExpression(styled, [css]) : t.taggedTemplateExpression(styled, css))])); for (const inserted of [].concat(insertedPaths)) { const initPath = inserted.get('declarations.0.init'); if (initPath.isTaggedTemplateExpression()) { taggedTemplateVisit(initPath, state); } else if (initPath.isCallExpression()) { callExpressionVisit(initPath, state); } } }; }; exports.default = _default;