├── .babelrc ├── .editorconfig ├── .eslintrc ├── .gitignore ├── .travis.yml ├── lib └── index.js ├── license.md ├── package.json ├── readme.md └── test ├── .eslintrc ├── fixtures ├── block-element │ ├── input.js │ └── output.js └── block │ ├── input.js │ └── output.js └── lib └── index.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ "es2015", "stage-2" ] 3 | } 4 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org/ 2 | root = true 3 | 4 | [*] 5 | end_of_line = lf 6 | insert_final_newline = true 7 | trim_trailing_whitespace = true 8 | charset = utf-8 9 | indent_style = space 10 | indent_size = 4 11 | 12 | [*.{json,yml}] 13 | indent_size = 2 14 | 15 | [{.babelrc,.eslintrc}] 16 | indent_size = 2 17 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "rebem/configs/common", 4 | "rebem/configs/babel" 5 | ], 6 | "rules": { 7 | "no-console": 0, 8 | "id-length": 0, 9 | "no-sync": 0 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | tmp/ 3 | build/ 4 | coverage/ 5 | *.sublime-* 6 | *.log 7 | .DS_Store 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # https://docs.travis-ci.com/user/customizing-the-build/ 2 | 3 | sudo: false 4 | 5 | language: node_js 6 | 7 | node_js: 8 | - "6" 9 | 10 | branches: 11 | only: 12 | - master 13 | 14 | matrix: 15 | fast_finish: true 16 | 17 | before_install: 18 | - npm install -g npm 19 | - npm --version 20 | 21 | script: npm start ci 22 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | import { stringify, validate } from 'rebem-classname'; 2 | 3 | // finds direct children of globalPath 4 | function findTopPath(path, globalPath) { 5 | if (path.parentPath === globalPath) { 6 | return path; 7 | } 8 | 9 | return findTopPath(path.parentPath, globalPath); 10 | } 11 | 12 | /* 13 | TODO: investigate why ES6 module import (below) doesn't create variable [UID] declaration 14 | [UID] is a generated scopedIdentifier — to avoid names collision 15 | 16 | var [UID] = require('babel-plugin-transform-rebem-jsx').checkBEMProps; 17 | 18 | */ 19 | function checkBEMExternal(t, scopedIdentifier) { 20 | return t.variableDeclaration( 21 | 'var', 22 | [ 23 | t.variableDeclarator( 24 | scopedIdentifier, 25 | t.memberExpression( 26 | t.callExpression( 27 | t.identifier('require'), 28 | [ 29 | t.stringLiteral('babel-plugin-transform-rebem-jsx') 30 | ] 31 | ), 32 | t.identifier('checkBEMProps') 33 | ) 34 | ) 35 | ] 36 | ); 37 | } 38 | 39 | export default function({ types: t }) { 40 | return { 41 | visitor: { 42 | Program: { 43 | exit(globalPath) { 44 | const checkBEMIdentifier = globalPath.scope.generateUidIdentifier('checkBEM'); 45 | let isCheckBEMInserted = false; 46 | 47 | globalPath.traverse({ 48 | CallExpression(path) { 49 | if ( 50 | t.isMemberExpression(path.node.callee) && 51 | t.isIdentifier(path.node.callee.object, { name: 'React' }) && 52 | t.isIdentifier(path.node.callee.property, { name: 'createElement' }) 53 | ) { 54 | if (!isCheckBEMInserted) { 55 | const topPath = findTopPath(path, globalPath); 56 | 57 | topPath.insertBefore( 58 | checkBEMExternal(t, checkBEMIdentifier) 59 | ); 60 | 61 | isCheckBEMInserted = true; 62 | } 63 | path.replaceWith( 64 | t.callExpression( 65 | checkBEMIdentifier, 66 | [ t.identifier('React') ].concat(path.node.arguments) 67 | ) 68 | ); 69 | } 70 | } 71 | }); 72 | } 73 | } 74 | } 75 | }; 76 | } 77 | 78 | const BEMProps = [ 'tag', 'block', 'elem', 'mods', 'mix', 'className' ]; 79 | let buildClassName = stringify; 80 | 81 | // validation 82 | // istanbul ignore next 83 | if (process.env.NODE_ENV !== 'production') { 84 | buildClassName = props => stringify(validate(props)); 85 | } 86 | 87 | // External helper for transforming BEM props into className 88 | /* eslint-disable */ 89 | export function checkBEMProps(React, element, props) { 90 | var finalProps = props; 91 | 92 | if (props && typeof element === 'string') { 93 | var className = buildClassName(props); 94 | var keyIndex; 95 | var key; 96 | var propsKeys = Object.keys(props); 97 | var propsLength = propsKeys.length; 98 | 99 | finalProps = {}; 100 | for (keyIndex = 0; keyIndex < propsLength; keyIndex++) { 101 | key = propsKeys[keyIndex]; 102 | 103 | if (BEMProps.indexOf(key) === -1) { 104 | finalProps[key] = props[key] 105 | } 106 | } 107 | 108 | if (className) { 109 | finalProps.className = className; 110 | } 111 | } 112 | 113 | return React.createElement.apply(React, [element, finalProps].concat([].slice.call(arguments, 3))); 114 | } 115 | /* eslint-enable */ 116 | -------------------------------------------------------------------------------- /license.md: -------------------------------------------------------------------------------- 1 | # The MIT License (MIT) 2 | 3 | * Copyright (c) 2015–present Kir Belevich 4 | * Copyright (c) 2015–present Denis Koltsov 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "babel-plugin-transform-rebem-jsx", 3 | "version": "0.3.3", 4 | "description": "Babel plugin for reBEM JSX transformations", 5 | "keywords": [ "rebem", "react", "bem", "jsx" ], 6 | "homepage": "https://github.com/rebem/rebem-jsx", 7 | "repository": "rebem/rebem-jsx", 8 | "maintainers": [ 9 | "Kir Belevich (https://github.com/deepsweet)", 10 | "Denis Koltsov (https://github.com/mistadikay)" 11 | ], 12 | "main": "build/index.js", 13 | "files": [ "build/" ], 14 | "dependencies": { 15 | "rebem-classname": "0.x.x" 16 | }, 17 | "devDependencies": { 18 | "start-babel-cli": "1.x.x", 19 | "start-rebem-preset": "0.x.x", 20 | 21 | "babel-core": "6.11.x", 22 | "babel-preset-es2015": "6.9.x", 23 | "babel-preset-stage-2": "6.11.x", 24 | "babel-preset-react": "6.11.x", 25 | "assert-diff": "1.0.x", 26 | 27 | "react": ">=15.0.0-rc.1", 28 | 29 | "babel-eslint": "6.1.x", 30 | "eslint-plugin-babel": "3.3.x", 31 | "eslint-config-rebem": "1.1.x", 32 | 33 | "husky": "0.11.x" 34 | }, 35 | "scripts": { 36 | "start": "start-runner start-rebem-preset", 37 | "prepush": "npm start prepush", 38 | "precommit": "npm start lint", 39 | "prepublish": "npm start build" 40 | }, 41 | "engines": { 42 | "node": ">=0.12.0", 43 | "npm": ">=2.7.0" 44 | }, 45 | "license": "MIT" 46 | } 47 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # babel-plugin-transform-rebem-jsx 2 | 3 | [![maintenance](https://img.shields.io/badge/maintained-no-red.svg?style=flat-square)](http://unmaintained.tech) 4 | [![npm](https://img.shields.io/npm/v/babel-plugin-transform-rebem-jsx.svg?style=flat-square)](https://www.npmjs.com/package/babel-plugin-transform-rebem-jsx) 5 | [![travis](http://img.shields.io/travis/rebem/rebem-jsx.svg?style=flat-square)](https://travis-ci.org/rebem/rebem-jsx) 6 | [![deps](http://img.shields.io/david/rebem/rebem-jsx.svg?style=flat-square)](https://david-dm.org/rebem/rebem-jsx) 7 | 8 | [Babel plugin](https://babeljs.io/docs/plugins/) allowing you to use BEM props for composing classNames in JSX like in [reBEM](https://github.com/rebem/rebem). 9 | 10 | ## Install 11 | 12 | ```sh 13 | $ npm i -S babel-plugin-transform-rebem-jsx 14 | ``` 15 | 16 | ### `.babelrc` 17 | 18 | ```js 19 | { 20 | "plugins": ["transform-rebem-jsx"] 21 | } 22 | ``` 23 | 24 | ## Usage 25 | 26 | ```js 27 |
28 |
29 |
30 |
31 | ``` 32 | 33 | ```html 34 |
35 |
36 |
37 |
38 | ``` 39 | 40 | ## Notes 41 | 42 | ### Environment variables 43 | 44 | `process.env.NODE_ENV` must be available. For example in webpack you can do this with `DefinePlugin`: 45 | 46 | ```js 47 | plugins: [ 48 | new webpack.DefinePlugin({ 49 | 'process.env': { 50 | NODE_ENV: JSON.stringify(process.env.NODE_ENV) 51 | } 52 | }) 53 | ] 54 | ``` 55 | 56 | ### Custom delimeters 57 | 58 | Default delimeters are `_` for modifiers and `__` for elements, but you can change it with special environment variables: 59 | 60 | ```js 61 | plugins: [ 62 | new webpack.DefinePlugin({ 63 | 'process.env': { 64 | REBEM_MOD_DELIM: JSON.stringify('--'), 65 | REBEM_ELEM_DELIM: JSON.stringify('~~') 66 | } 67 | }) 68 | ] 69 | ``` 70 | 71 | ### TODO 72 | - [x] docs 73 | - [x] move tasks to start-runner 74 | - [x] actual tests 75 | - [ ] more tests 76 | -------------------------------------------------------------------------------- /test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "rebem/configs/test" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/block-element/input.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | function Test() { 4 | return
5 |
6 |
; 7 | } 8 | -------------------------------------------------------------------------------- /test/fixtures/block-element/output.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _react = require("react"); 4 | 5 | var _react2 = _interopRequireDefault(_react); 6 | 7 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 8 | 9 | var _checkBEM = require("babel-plugin-transform-rebem-jsx").checkBEMProps; 10 | 11 | function Test() { 12 | return _checkBEM(_react2.default, "div", { block: "test" }, _checkBEM(_react2.default, "div", { block: "test", elem: "test2" })); 13 | } 14 | -------------------------------------------------------------------------------- /test/fixtures/block/input.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | function Test() { 4 | return
; 5 | } 6 | -------------------------------------------------------------------------------- /test/fixtures/block/output.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | var _react = require("react"); 4 | 5 | var _react2 = _interopRequireDefault(_react); 6 | 7 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 8 | 9 | var _checkBEM = require("babel-plugin-transform-rebem-jsx").checkBEMProps; 10 | 11 | function Test() { 12 | return _checkBEM(_react2.default, "div", { block: "test" }); 13 | } 14 | -------------------------------------------------------------------------------- /test/lib/index.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | import fs from 'fs'; 3 | import assert from 'assert-diff'; 4 | import React from 'react'; 5 | import { default as plugin, checkBEMProps } from '../../lib'; 6 | 7 | // babel-core supports only commonJS 8 | const babel = require('babel-core'); 9 | 10 | const pluginPath = require.resolve('../../lib'); 11 | const babelConfig = { 12 | presets: [ 'react' ], 13 | plugins: [ pluginPath ] 14 | }; 15 | 16 | function normalizeLines(str) { 17 | return str.trimRight().replace(/\r\n/g, '\n'); 18 | } 19 | 20 | describe('basic', function() { 21 | it('has default export', function() { 22 | assert(typeof plugin === 'function'); 23 | }); 24 | 25 | describe('checkBEMProps', function() { 26 | it('is a function', function() { 27 | assert(typeof checkBEMProps === 'function'); 28 | }); 29 | 30 | it('works with not defined props', function() { 31 | const element = checkBEMProps(React, 'span'); 32 | 33 | assert(typeof element === 'object'); 34 | assert(element.type === 'span'); 35 | }); 36 | 37 | it('works with null props', function() { 38 | const element = checkBEMProps(React, 'span', null); 39 | 40 | assert(typeof element === 'object'); 41 | assert(element.type === 'span'); 42 | }); 43 | 44 | it('creates element with correct props', function() { 45 | const element = checkBEMProps(React, 'div', { 46 | tag: 'span', 47 | block: 'block', 48 | elem: 'elem', 49 | mix: { 50 | block: 'block2' 51 | }, 52 | mods: { 53 | empty: true 54 | } 55 | }); 56 | 57 | assert(typeof element.props.tag === 'undefined'); 58 | assert(typeof element.props.block === 'undefined'); 59 | assert(typeof element.props.elem === 'undefined'); 60 | assert(typeof element.props.mix === 'undefined'); 61 | assert(typeof element.props.mods === 'undefined'); 62 | }); 63 | }); 64 | }); 65 | 66 | describe('fixtures', function() { 67 | const fixturesDirname = path.resolve(__dirname, '../fixtures'); 68 | 69 | fs.readdirSync(fixturesDirname) 70 | .map(fixturePath => path.resolve(fixturesDirname, fixturePath)) 71 | .filter(fixturePath => fs.lstatSync(fixturePath).isDirectory()) 72 | .forEach(fixturePath => { 73 | const testCase = path.parse(fixturePath).name; 74 | 75 | it(testCase, function() { 76 | const inputPath = path.resolve(fixturePath, 'input.js'); 77 | const actual = babel.transformFileSync(inputPath, babelConfig).code; 78 | const outputPath = path.resolve(fixturePath, 'output.js'); 79 | const expected = fs.readFileSync(outputPath, 'utf-8'); 80 | 81 | assert.equal( 82 | normalizeLines(actual), 83 | normalizeLines(expected) 84 | ); 85 | }); 86 | }); 87 | }); 88 | --------------------------------------------------------------------------------