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
JavaScript
;
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;