From f038e656589a05825e044224d0e7abc94682f292 Mon Sep 17 00:00:00 2001 From: Pines-Cheng Date: Sat, 25 Aug 2018 14:27:07 +0800 Subject: [PATCH] =?UTF-8?q?feat(RN):=20=20=E6=B7=BB=E5=8A=A0=20babel-plugi?= =?UTF-8?q?n-transform-jsx-to-stylesheet?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lerna.json | 3 +- .../README.md | 154 ++++++++++ .../package.json | 25 ++ .../src/__tests__/index.js | 276 ++++++++++++++++++ .../src/index.js | 241 +++++++++++++++ 5 files changed, 698 insertions(+), 1 deletion(-) create mode 100644 packages/babel-plugin-transform-jsx-to-stylesheet/README.md create mode 100644 packages/babel-plugin-transform-jsx-to-stylesheet/package.json create mode 100644 packages/babel-plugin-transform-jsx-to-stylesheet/src/__tests__/index.js create mode 100644 packages/babel-plugin-transform-jsx-to-stylesheet/src/index.js diff --git a/lerna.json b/lerna.json index 9f78899c1b7f..18515d7a730c 100644 --- a/lerna.json +++ b/lerna.json @@ -27,7 +27,8 @@ "packages/eslint-config-taro", "packages/eslint-plugin-taro", "packages/taro-transformer-wx", - "packages/postcss-pxtransform" + "packages/postcss-pxtransform", + "packages/babel-plugin-transform-jsx-to-stylesheet" ], "command": { "publish": { diff --git a/packages/babel-plugin-transform-jsx-to-stylesheet/README.md b/packages/babel-plugin-transform-jsx-to-stylesheet/README.md new file mode 100644 index 000000000000..39c259c724e6 --- /dev/null +++ b/packages/babel-plugin-transform-jsx-to-stylesheet/README.md @@ -0,0 +1,154 @@ +# babel-plugin-transform-jsx-stylesheet +> Transform StyleSheet selector to style in JSX Elements. + +## Installation + +```sh +npm install --save-dev babel-plugin-transform-jsx-stylesheet +``` + +## Usage + +### Via `.babelrc` + +**.babelrc** + +```json +{ + "plugins": ["transform-jsx-stylesheet"] +} +``` + +## Example + +Your `component.js` that contains this code: + +```js +import { createElement, Component } from 'rax'; +import './app.css'; + +class App extends Component { + render() { + return
+ } +} +``` + +Will be transpiled into something like this: + +```js +import { createElement, Component } from 'rax'; +import appStyleSheet from './app.css'; + +class App extends Component { + render() { + return
; + } +} + +const styleSheet = appStyleSheet; +``` + +Can write multiple classNames like this: + +```js +import { createElement, Component } from 'rax'; +import './app.css'; + +class App extends Component { + render() { + return
; + } +} +``` + +Will be transpiled into something like this: + +```js +import { createElement, Component } from 'rax'; +import appStyleSheet from './app.css'; + +class App extends Component { + render() { + return
; + } +} + +const styleSheet = appStyleSheet; +``` + +Also support array, object and expressions like this: **(since 0.6.0)** + +```js +import { createElement, Component } from 'rax'; +import './app.css'; + +class App extends Component { + render() { + return ( +
+
+
+
+
+
+ ); + } +} +``` + +Will be transpiled into something like this: + +```js +import { createElement, Component } from 'rax'; +import appStyleSheet from './app.css'; + +class App extends Component { + render() { + return ( +
+
+
+
+
+
+ ); + } +} + +const styleSheet = appStyleSheet; +function _getClassName() { /* codes */ } +function _getStyle(className) { + return styleSheet[_getClassName(className)]; // not real code +} +``` + +And can also import multiple css file: + +```js +import { createElement, Component } from 'rax'; +import 'app1.css'; +import 'app2.css'; + +class App extends Component { + render() { + return
; + } +} +``` + +Will be transpiled into something like this: + +```js +import { createElement, Component } from 'rax'; +import app1StyleSheet from 'app1.css'; +import app2StyleSheet from 'app2.css'; + +class App extends Component { + render() { + return
; + } +} + +const styleSheet = Object.assign({}, app1StyleSheet, app2StyleSheet); +``` diff --git a/packages/babel-plugin-transform-jsx-to-stylesheet/package.json b/packages/babel-plugin-transform-jsx-to-stylesheet/package.json new file mode 100644 index 000000000000..06ad7cee8ae0 --- /dev/null +++ b/packages/babel-plugin-transform-jsx-to-stylesheet/package.json @@ -0,0 +1,25 @@ +{ + "name": "babel-plugin-transform-jsx-to-stylesheet", + "version": "0.6.5", + "description": "Transform stylesheet selector to style in JSX Elements.", + "license": "MIT", + "main": "src/index.js", + "repository": { + "type": "git", + "url": "git+https://github.com/NervJS/taro.git" + }, + "bugs": { + "url": "https://github.com/NervJS/taro/issues" + }, + "homepage": "https://github.com/NervJS/taro#readme", + "engines": { + "npm": ">=3.0.0" + }, + "devDependencies": { + "babel-core": "^6.23.1", + "babel-plugin-syntax-jsx": "^6.18.0" + }, + "dependencies": { + "camelcase": "^3.0.0" + } +} diff --git a/packages/babel-plugin-transform-jsx-to-stylesheet/src/__tests__/index.js b/packages/babel-plugin-transform-jsx-to-stylesheet/src/__tests__/index.js new file mode 100644 index 000000000000..575f9a9ff6be --- /dev/null +++ b/packages/babel-plugin-transform-jsx-to-stylesheet/src/__tests__/index.js @@ -0,0 +1,276 @@ +import jSXStylePlugin from '../index' +import syntaxJSX from 'babel-plugin-syntax-jsx' +import { transform } from 'babel-core' + +const mergeStylesFunctionTemplate = `function _mergeStyles() { + 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 = `function _getClassName() { + 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 = _getClassName(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 = `function _getStyle(classNameExpression) { + var cache = _styleSheet.__cache || (_styleSheet.__cache = {}); + + var className = _getClassName(classNameExpression);\n + var classNameArr = className.split(/\\s+/); + var style = cache[className]; + + if (!style) { + style = {}; + + if (classNameArr.length === 1) { + style = _styleSheet[classNameArr[0].trim()]; + } else { + classNameArr.forEach(function (cls) { + style = Object.assign(style, _styleSheet[cls.trim()]); + }); + } + + cache[className] = style; + } + + return style; +}` + +describe('jsx style plugin', () => { + function getTransfromCode (code) { + return transform(code, { + plugins: [jSXStylePlugin, syntaxJSX] + }).code + } + + it('transform only one className to style as member', () => { + expect(getTransfromCode(` +import { createElement, Component } from 'rax'; +import './app.css'; + +class App extends Component { + render() { + return
; + } +}`)).toBe(` +import { createElement, Component } from 'rax'; +import appStyleSheet from './app.css'; + +var _styleSheet = appStyleSheet; +class App extends Component { + render() { + return
; + } +}`) + }) + + it('transform multiple classNames to style as array', () => { + expect(getTransfromCode(` +import { createElement, Component } from 'rax'; +import './app.css'; + +class App extends Component { + render() { + return
; + } +}`)).toBe(` +import { createElement, Component } from 'rax'; +import appStyleSheet from './app.css'; + +var _styleSheet = appStyleSheet; +class App extends Component { + render() { + return
; + } +}`) + }) + + it('transform array, object and expressions', () => { + expect(getTransfromCode(` +import { createElement, Component } from 'rax'; +import './app.css'; + +class App extends Component { + render() { + return
+
+
+
+
+
; + } +}`)).toBe(` +import { createElement, Component } from 'rax'; +import appStyleSheet from './app.css'; + +var _styleSheet = appStyleSheet; + +${getClassNameFunctionTemplate} + +${getStyleFunctionTemplete} + +class App extends Component { + render() { + return
+
+
+
+
+
; + } +}`) + }) + + it('combine one style and className', () => { + expect(getTransfromCode(` +import { createElement, Component } from 'rax'; +import './app.css'; +import style from './style.css'; + +class App extends Component { + render() { + return
; + } +}`)).toBe(`${mergeStylesFunctionTemplate} + +import { createElement, Component } from 'rax'; +import appStyleSheet from './app.css'; +import styleStyleSheet from './style.css'; + +var _styleSheet = _mergeStyles(appStyleSheet, styleStyleSheet); + +class App extends Component { + render() { + return
; + } +}`) + }) + + it('combine inline style object and className', () => { + expect(getTransfromCode(` +import { createElement, Component } from 'rax'; +import './app.css'; + +class App extends Component { + render() { + return
; + } +}`)).toBe(` +import { createElement, Component } from 'rax'; +import appStyleSheet from './app.css'; + +var _styleSheet = appStyleSheet; +class App extends Component { + render() { + return
; + } +}`) + }) + + it('combine multiple styles and className', () => { + expect(getTransfromCode(` +import { createElement, Component } from 'rax'; +import './app.css'; +import style from './style.css'; + +class App extends Component { + render() { + return
; + } +}`)).toBe(`${mergeStylesFunctionTemplate} + +import { createElement, Component } from 'rax'; +import appStyleSheet from './app.css'; +import styleStyleSheet from './style.css'; + +var _styleSheet = _mergeStyles(appStyleSheet, styleStyleSheet); + +class App extends Component { + render() { + return
; + } +}`) + }) + + it('do not transfrom code when no css file', () => { + const code = ` +import { createElement, Component } from 'rax'; + +class App extends Component { + render() { + return
; + } +}` + + expect(getTransfromCode(code)).toBe(code) + }) + + it('transform scss file', () => { + expect(getTransfromCode(` +import { createElement, Component } from 'rax'; +import './app.scss'; + +class App extends Component { + render() { + return
; + } +}`)).toBe(` +import { createElement, Component } from 'rax'; +import appStyleSheet from './app.scss'; + +var _styleSheet = appStyleSheet; +class App extends Component { + render() { + return
; + } +}`) + }) + + it('transform constant elements in render', () => { + expect(getTransfromCode(` +import { createElement, render } from 'rax'; +import './app.css'; + +render(
); +`)).toBe(` +import { createElement, render } from 'rax'; +import appStyleSheet from './app.css'; + +var _styleSheet = appStyleSheet; +render(
);`) + }) +}) diff --git a/packages/babel-plugin-transform-jsx-to-stylesheet/src/index.js b/packages/babel-plugin-transform-jsx-to-stylesheet/src/index.js new file mode 100644 index 000000000000..b176a3881e41 --- /dev/null +++ b/packages/babel-plugin-transform-jsx-to-stylesheet/src/index.js @@ -0,0 +1,241 @@ +import path from 'path' +import camelcase from 'camelcase' + +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'] + +export default 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 cache = ${STYLE_SHEET_NAME}.__cache || (${STYLE_SHEET_NAME}.__cache = {}); + var className = ${GET_CLS_NAME_FUNC_NAME}(classNameExpression); + var classNameArr = className.split(/\\s+/); + var style = cache[className]; + + if (!style) { + style = {}; + if (classNameArr.length === 1) { + style = ${STYLE_SHEET_NAME}[classNameArr[0].trim()]; + } else { + classNameArr.forEach(function(cls) { + style = Object.assign(style, ${STYLE_SHEET_NAME}[cls.trim()]); + }); + } + cache[className] = style; + } + + 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) + } + } + + 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 ({node}, {file}) { + const sourceValue = node.source.value + const extname = path.extname(sourceValue) + const cssIndex = cssSuffixs.indexOf(extname) + // Do not convert `import styles from './foo.css'` kind + if (node.importKind !== 'value' && cssIndex > -1) { + let cssFileCount = file.get('cssFileCount') || 0 + let cssParamIdentifiers = file.get('cssParamIdentifiers') || [] + const cssFileBaseName = camelcase(path.basename(sourceValue, extname)) + const styleSheetIdentifier = t.identifier(`${cssFileBaseName + NAME_SUFFIX}`) + + node.specifiers = [t.importDefaultSpecifier(styleSheetIdentifier)] + cssParamIdentifiers.push(styleSheetIdentifier) + cssFileCount++ + + file.set('cssParamIdentifiers', cssParamIdentifiers) + file.set('cssFileCount', cssFileCount) + } + } + } + } +};