// @flow import { compile } from 'stylis' const haveSameLocation = (element1, element2) => { return element1.line === element2.line && element1.column === element2.column } const isAutoInsertedRule = element => element.type === 'rule' && element.parent && haveSameLocation(element, element.parent) const toInputTree = (elements, tree) => { for (let i = 0; i < elements.length; i++) { const element = elements[i] const { parent, children } = element if (!parent) { tree.push(element) } else if (!isAutoInsertedRule(element)) { parent.children.push(element) } if (Array.isArray(children)) { element.children = [] toInputTree(children, tree) } } return tree } var stringifyTree = elements => { return elements .map(element => { switch (element.type) { case 'import': case 'decl': return element.value case 'comm': // When we encounter a standard multi-line CSS comment and it contains a '@' // character, we keep the comment. Some Stylis plugins, such as // the stylis-rtl via the cssjanus plugin, use this special comment syntax // to control behavior (such as: /* @noflip */). We can do this // with standard CSS comments because they will work with compression, // as opposed to non-standard single-line comments that will break compressed CSS. return element.props === '/' && element.value.includes('@') ? element.value : '' case 'rule': return `${element.value.replace(/&\f/g, '&')}{${stringifyTree( element.children )}}` default: { return `${element.value}{${stringifyTree(element.children)}}` } } }) .join('') } const interleave = (strings: Array<*>, interpolations: Array<*>) => interpolations.reduce( (array, interp, i) => array.concat([interp], strings[i + 1]), [strings[0]] ) function getDynamicMatches(str: string) { const re = /xxx(\d+):xxx/gm let match const matches = [] while ((match = re.exec(str)) !== null) { // so that flow doesn't complain if (match !== null) { matches.push({ value: match[0], p1: parseInt(match[1], 10), index: match.index }) } } return matches } function replacePlaceholdersWithExpressions( str: string, expressions: Array<*>, t: * ) { const matches = getDynamicMatches(str) if (matches.length === 0) { if (str === '') { return [] } return [t.stringLiteral(str)] } const strings = [] const finalExpressions = [] let cursor = 0 matches.forEach(({ value, p1, index }, i) => { const preMatch = str.substring(cursor, index) cursor = cursor + preMatch.length + value.length if (!preMatch && i === 0) { strings.push(t.stringLiteral('')) } else { strings.push(t.stringLiteral(preMatch)) } finalExpressions.push(expressions[p1]) if (i === matches.length - 1) { strings.push(t.stringLiteral(str.substring(index + value.length))) } }) return interleave(strings, finalExpressions).filter( (node: { value: string }) => { return node.value !== '' } ) } function createRawStringFromTemplateLiteral(quasi: { quasis: Array<{ value: { cooked: string } }> }) { let strs = quasi.quasis.map(x => x.value.cooked) const src = strs .reduce((arr, str, i) => { arr.push(str) if (i !== strs.length - 1) { arr.push(`xxx${i}:xxx`) } return arr }, []) .join('') .trim() return src } export default function minify(path: *, t: *): void { const quasi = path.node.quasi const raw = createRawStringFromTemplateLiteral(quasi) const minified = stringifyTree(toInputTree(compile(raw), [])) const expressions = replacePlaceholdersWithExpressions( minified, quasi.expressions || [], t ) path.replaceWith(t.callExpression(path.node.tag, expressions)) }