diff --git a/src/utils/helpers.js b/src/utils/helpers.js index da09de1..04c5b0a 100644 --- a/src/utils/helpers.js +++ b/src/utils/helpers.js @@ -4,6 +4,7 @@ import path from 'path'; import generate from '@babel/generator'; import _ from 'lodash'; import hash from 'object-hash'; +import { isStringLike } from './strings'; export function flattenLogicalExpression(rootNode) { const result = []; @@ -130,22 +131,13 @@ export function isSafeConditionalExpression(node) { const { consequent, alternate } = node; - if ( - t.isStringLiteral(consequent) && - t.isStringLiteral(alternate) && - consequent.value.length > 0 && - alternate.value.length > 0 - ) { + if (isStringLike(consequent) && isStringLike(alternate)) { return true; } if ( - (t.isStringLiteral(consequent) && - consequent.value.length > 0 && - isSafeConditionalExpression(alternate)) || - (t.isStringLiteral(alternate) && - alternate.value.length > 0 && - isSafeConditionalExpression(consequent)) + (isStringLike(consequent) && isSafeConditionalExpression(alternate)) || + (isStringLike(alternate) && isSafeConditionalExpression(consequent)) ) { return true; } diff --git a/src/utils/strings.js b/src/utils/strings.js new file mode 100644 index 0000000..c20063e --- /dev/null +++ b/src/utils/strings.js @@ -0,0 +1,80 @@ +import * as t from '@babel/types'; + +export function isStringLike(node) { + return t.isStringLiteral(node) || t.isTemplateLiteral(node); +} + +export function combineStringLike(a, b) { + if (isStringLikeEmpty(a) && isStringLikeEmpty(b)) { + return t.stringLiteral(''); + } + + if (t.isStringLiteral(a) && t.isStringLiteral(b)) { + return t.stringLiteral(a.value + ' ' + b.value); + } + + if (t.isTemplateLiteral(a) && t.isTemplateLiteral(b)) { + const expressions = [...a.expressions, ...b.expressions]; + const quasis = [...a.quasis]; + + quasis[quasis.length - 1] = templateElement( + quasis[quasis.length - 1].value.raw + ' ' + b.quasis[0].value.raw, + ); + + quasis.push(...b.quasis.slice(1)); + + return templateOrStringLiteral(quasis, expressions); + } + + if (t.isTemplateLiteral(a) && t.isStringLiteral(b)) { + const expressions = [...a.expressions]; + const quasis = [...a.quasis]; + + const i = quasis.length - 1; + quasis[i] = templateElement(quasis[i].value.raw + ' ' + b.value, true); + + return templateOrStringLiteral(quasis, expressions); + } + + if (t.isStringLiteral(a) && t.isTemplateLiteral(b)) { + const expressions = [...b.expressions]; + const quasis = [...b.quasis]; + + const i = 0; + quasis[i] = templateElement(a.value + ' ' + quasis[i].value.raw, true); + + return templateOrStringLiteral(quasis, expressions); + } + + throw new Error('Unable to handle that input'); +} + +export function isStringLikeEmpty(node) { + if (t.isStringLiteral(node)) { + return node.value.length === 0; + } + + if (t.isTemplateLiteral(node)) { + return node.expressions.length === 0 && node.quasis.every(q => q.value.raw.length === 0); + } + + return false; +} + +function templateOrStringLiteral(quasis, expressions) { + if (expressions.length === 0) { + return t.stringLiteral(quasis[0].value.raw); + } + + return t.templateLiteral(quasis, expressions); +} + +function templateElement(value, tail = false) { + return { + type: 'TemplateElement', + value: { + raw: value, + }, + tail, + }; +} diff --git a/src/visitors/combineStringLiterals.js b/src/visitors/combineStringLiterals.js index 4d92017..50977e8 100644 --- a/src/visitors/combineStringLiterals.js +++ b/src/visitors/combineStringLiterals.js @@ -1,79 +1,19 @@ import * as t from '@babel/types'; import _ from 'lodash'; +import { isStringLike, combineStringLike } from '../utils/strings'; function combineStringsInArray(array) { if (array.length < 2) { return array; } - const [match, noMatch] = _.partition( - array, - item => t.isStringLiteral(item) || t.isTemplateLiteral(item), - ); + const [match, noMatch] = _.partition(array, isStringLike); if (match.length < 2) { return array; } - const [strings, templates] = _.partition(match, t.isStringLiteral); - - const expressions = []; - const quasis = []; - - // Combine string literals - if (strings.length > 0) { - quasis.push( - templateElement( - strings.reduce((prev, curr) => t.stringLiteral(prev.value + ' ' + curr.value)).value, - true, - ), - ); - } - - // Combine template literals - templates.forEach(item => { - if (item.expressions.length === 0) { - const newValue = item.quasis.reduce((prev, curr) => - templateElement(prev.value.raw + ' ' + curr.value.raw, true), - ); - - if (quasis.length === 0) { - quasis.push(newValue); - return; - } - - const prevItem = quasis[quasis.length - 1]; - quasis[quasis.length - 1] = templateElement( - prevItem.value.raw + ' ' + newValue.value.raw, - true, - ); - return; - } - - item.quasis.forEach(item => { - if (quasis.length !== 0) { - const prevItem = quasis[quasis.length - 1]; - - if (item.tail === false && prevItem.tail) { - quasis[quasis.length - 1] = templateElement( - prevItem.value.raw + ' ' + item.value.raw, - true, - ); - return; - } - } - - quasis.push(item); - }); - - expressions.push(...item.expressions); - }); - - if (expressions.length === 0 && quasis.length === 1) { - return [t.stringLiteral(quasis[0].value.raw), ...noMatch]; - } - - return [t.templateLiteral(quasis, expressions), ...noMatch]; + return [match.reduce(combineStringLike), ...noMatch]; } const arrayVisitor = { @@ -102,13 +42,3 @@ const visitor = { export default (path, options) => { path.traverse(visitor, { options }); }; - -function templateElement(value, tail = false) { - return { - type: 'TemplateElement', - value: { - raw: value, - }, - tail, - }; -} diff --git a/src/visitors/removeUnnecessaryCalls.js b/src/visitors/removeUnnecessaryCalls.js index 848e6ad..42c5baf 100644 --- a/src/visitors/removeUnnecessaryCalls.js +++ b/src/visitors/removeUnnecessaryCalls.js @@ -1,5 +1,6 @@ import * as t from '@babel/types'; import { isSafeConditionalExpression } from '../utils/helpers'; +import { isStringLike, combineStringLike, isStringLikeEmpty } from '../utils/strings'; const transforms = [ function noArgumentsToString(args) { @@ -9,7 +10,7 @@ const transforms = [ }, function singleStringLiteral(args) { - if (args.length === 1 && (t.isStringLiteral(args[0]) || t.isTemplateLiteral(args[0]))) { + if (args.length === 1 && isStringLike(args[0])) { return args[0]; } }, @@ -25,11 +26,11 @@ const transforms = [ const [arg1, arg2] = args; - if (isSafeConditionalExpression(arg1) && isSafeConditionalExpression(arg2)) { + if (args.every(isSafeConditionalExpression)) { const newCond = t.conditionalExpression( arg1.test, - t.stringLiteral(arg1.consequent.value + ' '), - t.stringLiteral(arg1.alternate.value + ' '), + combineStringLike(arg1.consequent, t.stringLiteral('')), + combineStringLike(arg1.alternate, t.stringLiteral('')), ); return t.binaryExpression('+', newCond, arg2); @@ -41,14 +42,23 @@ const transforms = [ const [arg1, arg2] = args; - if ( - (t.isStringLiteral(arg1) || t.isStringLiteral(arg2)) && - (isSafeConditionalExpression(arg1) || isSafeConditionalExpression(arg2)) - ) { - const string = t.isStringLiteral(arg1) ? arg1 : arg2; - const conditional = t.isStringLiteral(arg2) ? arg1 : arg2; + if (args.some(isStringLike) && args.some(isSafeConditionalExpression)) { + const string = isStringLike(arg1) ? arg1 : arg2; + const conditional = isStringLike(arg2) ? arg1 : arg2; + + if (isStringLikeEmpty(conditional.consequent) || isStringLikeEmpty(conditional.alternate)) { + return t.binaryExpression( + '+', + string, + t.conditionalExpression( + conditional.test, + combineStringLike(t.stringLiteral(''), conditional.consequent), + combineStringLike(t.stringLiteral(''), conditional.alternate), + ), + ); + } - return t.binaryExpression('+', t.stringLiteral(string.value + ' '), conditional); + return t.binaryExpression('+', combineStringLike(string, t.stringLiteral('')), conditional); } }, @@ -56,9 +66,9 @@ const transforms = [ if (args.length !== 1) return; const [arg] = args; - if (t.isLogicalExpression(arg, { operator: '&&' }) && t.isStringLiteral(arg.right)) { + if (t.isLogicalExpression(arg, { operator: '&&' }) && isStringLike(arg.right)) { return t.conditionalExpression(arg.left, arg.right, t.stringLiteral('')); - } else if (t.isLogicalExpression(arg, { operator: '||' }) && t.isStringLiteral(arg.right)) { + } else if (t.isLogicalExpression(arg, { operator: '||' }) && isStringLike(arg.right)) { // Assume that arg.left returns a string value return arg; } @@ -69,16 +79,16 @@ const transforms = [ const [arg1, arg2] = args; if ( - t.isStringLiteral(arg1) && + isStringLike(arg1) && t.isLogicalExpression(arg2, { operator: '&&' }) && - t.isStringLiteral(arg2.right) + isStringLike(arg2.right) ) { return t.binaryExpression( '+', arg1, t.conditionalExpression( arg2.left, - t.stringLiteral(' ' + arg2.right.value), + combineStringLike(t.stringLiteral(''), arg2.right), t.stringLiteral(''), ), ); diff --git a/test/fixtures/combine-string-literals/string-and-template/code.js b/test/fixtures/combine-string-literals/string-and-template/code.js new file mode 100644 index 0000000..f64a6a3 --- /dev/null +++ b/test/fixtures/combine-string-literals/string-and-template/code.js @@ -0,0 +1,2 @@ +const x = clsx('foo', `bar-${baz}`); +const y = clsx(`bar-${baz}`, 'foo'); diff --git a/test/fixtures/combine-string-literals/string-and-template/output.js b/test/fixtures/combine-string-literals/string-and-template/output.js new file mode 100644 index 0000000..11f7bb1 --- /dev/null +++ b/test/fixtures/combine-string-literals/string-and-template/output.js @@ -0,0 +1,2 @@ +const x = clsx(`foo bar-${baz}`); +const y = clsx(`bar-${baz} foo`); diff --git a/test/fixtures/remove-unnecessary-calls/string-and-conditional-argument/code.js b/test/fixtures/remove-unnecessary-calls/string-and-conditional-argument/code.js index 5d15cb9..ec61294 100644 --- a/test/fixtures/remove-unnecessary-calls/string-and-conditional-argument/code.js +++ b/test/fixtures/remove-unnecessary-calls/string-and-conditional-argument/code.js @@ -1,2 +1,4 @@ -const classA = clsx('foo bar', foo ? 'a' : 'b'); -const classB = clsx(foo ? 'a' : 'b', 'foo bar'); +const x1 = clsx('foo bar', foo ? 'a' : 'b'); +const x2 = clsx(foo ? 'a' : 'b', 'foo bar'); +const x3 = clsx('foo bar', foo ? 'a' : ``); +const x4 = clsx('foo bar', foo ? 'a' : ''); diff --git a/test/fixtures/remove-unnecessary-calls/string-and-conditional-argument/output.js b/test/fixtures/remove-unnecessary-calls/string-and-conditional-argument/output.js index 6e0ad72..8a0ed04 100644 --- a/test/fixtures/remove-unnecessary-calls/string-and-conditional-argument/output.js +++ b/test/fixtures/remove-unnecessary-calls/string-and-conditional-argument/output.js @@ -1,2 +1,4 @@ -const classA = 'foo bar ' + (foo ? 'a' : 'b'); -const classB = 'foo bar ' + (foo ? 'a' : 'b'); +const x1 = 'foo bar ' + (foo ? 'a' : 'b'); +const x2 = 'foo bar ' + (foo ? 'a' : 'b'); +const x3 = 'foo bar' + (foo ? ' a' : ''); +const x4 = 'foo bar' + (foo ? ' a' : '');