saven
Version:
247 lines (214 loc) • 8.79 kB
JavaScript
const path = require('path')
const STYLE_SHEET_NAME = '_styleSheet'
const GET_STYLE_FUNC_NAME = '_getStyle'
const MERGE_STYLES_FUNC_NAME = '_mergeStyles'
const GET_CLS_NAME_FUNC_NAME = '_getClassName'
const NAME_SUFFIX = 'StyleSheet'
const cssSuffixs = ['.css', '.scss', '.sass', '.less']
module.exports = function ({types: t, template}) {
const mergeStylesFunctionTemplate = template(`
function ${MERGE_STYLES_FUNC_NAME}() {
var newTarget = {};
for (var index = 0; index < arguments.length; index++) {
var target = arguments[index];
for (var key in target) {
newTarget[key] = Object.assign(newTarget[key] || {}, target[key]);
}
}
return newTarget;
}
`)
const getClassNameFunctionTemplate = template(`
function ${GET_CLS_NAME_FUNC_NAME}() {
var className = [];
var args = arguments[0];
var type = Object.prototype.toString.call(args).slice(8, -1).toLowerCase();
if (type === 'string') {
args = args.trim();
args && className.push(args);
} else if (type === 'array') {
args.forEach(function (cls) {
cls = ${GET_CLS_NAME_FUNC_NAME}(cls).trim();
cls && className.push(cls);
});
} else if (type === 'object') {
for (var k in args) {
k = k.trim();
if (k && args.hasOwnProperty(k) && args[k]) {
className.push(k);
}
}
}
return className.join(' ').trim();
}
`)
const getStyleFunctionTemplete = template(`
function ${GET_STYLE_FUNC_NAME}(classNameExpression) {
var className = ${GET_CLS_NAME_FUNC_NAME}(classNameExpression);
var classNameArr = className.split(/\\s+/);
var style = [];
if (classNameArr.length === 1) {
style.push(${STYLE_SHEET_NAME}[classNameArr[0].trim()]);
} else {
classNameArr.forEach(function(cls) {
style.push(${STYLE_SHEET_NAME}[cls.trim()]);
});
}
return style;
}
`)
const getClassNameFunctionAst = getClassNameFunctionTemplate()
const mergeStylesFunctionAst = mergeStylesFunctionTemplate()
const getStyleFunctionAst = getStyleFunctionTemplete()
function getArrayExpression (value) {
let expression
let str
if (!value || value.value === '') {
// className
// className=""
return []
} else if (value.type === 'JSXExpressionContainer' && value.expression && typeof value.expression.value !== 'string') {
// className={{ container: true }}
// className={['container wrapper', { scroll: false }]}
return [t.callExpression(t.identifier(GET_STYLE_FUNC_NAME), [value.expression])]
} else {
// className="container"
// className={'container'}
str = (value.expression ? value.expression.value : value.value).trim()
}
return str === '' ? [] : str.split(/\s+/).map((className) => {
return template(`${STYLE_SHEET_NAME}["${className}"]`)().expression
})
}
function findLastImportIndex (body) {
const bodyReverse = body.slice(0).reverse()
let _index = 0
bodyReverse.some((node, index) => {
if (node.type === 'ImportDeclaration') {
_index = body.length - index - 1
return true
}
return false
})
return _index
}
return {
visitor: {
Program: {
exit ({node}, {file}) {
// const cssFileCount = file.get('cssFileCount')
const injectGetStyle = file.get('injectGetStyle')
const lastImportIndex = findLastImportIndex(node.body)
let cssParamIdentifiers = file.get('cssParamIdentifiers')
let callExpression
if (cssParamIdentifiers) {
// only one css file,由于样式文件合并,永远只有一个
if (cssParamIdentifiers.length === 1) {
callExpression = t.variableDeclaration('var', [t.variableDeclarator(t.identifier(STYLE_SHEET_NAME), cssParamIdentifiers[0])])
} else if (cssParamIdentifiers.length > 1) {
const objectAssignExpression = t.callExpression(t.identifier(MERGE_STYLES_FUNC_NAME), cssParamIdentifiers)
callExpression = t.variableDeclaration('var', [t.variableDeclarator(t.identifier(STYLE_SHEET_NAME), objectAssignExpression)])
}
node.body.splice(lastImportIndex + 1, 0, callExpression)
if (injectGetStyle) {
node.body.splice(lastImportIndex + 2, 0, getClassNameFunctionAst)
node.body.splice(lastImportIndex + 3, 0, getStyleFunctionAst)
}
}
// 全部合并成一个,也就不需要 mergeStylesFunctionAst 了
// if (cssFileCount > 1) {
// node.body.unshift(mergeStylesFunctionAst)
// }
}
},
JSXOpeningElement ({container}, {file}) {
const cssFileCount = file.get('cssFileCount') || 0
if (cssFileCount < 1) {
return
}
// Check if has "style"
let hasStyleAttribute = false
let styleAttribute
let hasClassName = false
let classNameAttribute
const attributes = container.openingElement.attributes
for (let i = 0; i < attributes.length; i++) {
const name = attributes[i].name
if (name) {
if (!hasStyleAttribute) {
hasStyleAttribute = name.name === 'style'
styleAttribute = hasStyleAttribute && attributes[i]
}
if (!hasClassName) {
hasClassName = name.name === 'className'
classNameAttribute = hasClassName && attributes[i]
}
}
}
if (hasClassName) {
// Remove origin className
attributes.splice(attributes.indexOf(classNameAttribute), 1)
if (
classNameAttribute.value &&
classNameAttribute.value.type === 'JSXExpressionContainer' &&
typeof classNameAttribute.value.expression.value !== 'string' // not like className={'container'}
) {
file.set('injectGetStyle', true)
}
const arrayExpression = getArrayExpression(classNameAttribute.value)
if (arrayExpression.length === 0) {
return
}
if (hasStyleAttribute && styleAttribute.value) {
let expression = styleAttribute.value.expression
let expressionType = expression.type
// style={[styles.a, styles.b]} ArrayExpression
if (expressionType === 'ArrayExpression') {
expression.elements = arrayExpression.concat(expression.elements)
// style={styles.a} MemberExpression
// style={{ height: 100 }} ObjectExpression
// style={{ ...custom }} ObjectExpression
// style={custom} Identifier
// style={getStyle()} CallExpression
// style={this.props.useCustom ? custom : null} ConditionalExpression
// style={custom || other} LogicalExpression
} else {
styleAttribute.value.expression = t.arrayExpression(arrayExpression.concat(expression))
}
} else {
let expression = arrayExpression.length === 1 ? arrayExpression[0] : t.arrayExpression(arrayExpression)
attributes.push(t.jSXAttribute(t.jSXIdentifier('style'), t.jSXExpressionContainer(expression)))
}
}
},
// 由于目前 js 引入的文件样式默认会全部合并,故进插入一个就好,其余的全部 remove
ImportDeclaration (astPath, {file}) {
const node = astPath.node
const sourceValue = node.source.value
const extname = path.extname(sourceValue)
const cssIndex = cssSuffixs.indexOf(extname)
let cssFileCount = file.get('cssFileCount') || 0
let cssParamIdentifiers = file.get('cssParamIdentifiers') || []
// Do not convert `import styles from './foo.css'` kind
if (node.importKind !== 'value' && cssIndex > -1) {
// 第一个引入的样式文件
if (cssFileCount === 0) {
const cssFileBaseName = path.basename(sourceValue, extname)
// 引入样式对应的变量名
const styleSheetIdentifierValue = `${cssFileBaseName + NAME_SUFFIX}`
const styleSheetIdentifierPath = `${path.dirname(sourceValue)}/${cssFileBaseName}_styles`
const styleSheetIdentifier = t.identifier(styleSheetIdentifierValue)
node.specifiers = [t.importDefaultSpecifier(styleSheetIdentifier)]
node.source = t.stringLiteral(styleSheetIdentifierPath)
cssParamIdentifiers.push(styleSheetIdentifier)
} else {
astPath.remove()
}
cssFileCount++
file.set('cssParamIdentifiers', cssParamIdentifiers)
file.set('cssFileCount', cssFileCount)
}
}
}
}
}