diff --git a/README.md b/README.md index e57f452..e403c84 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ Out of the box it comes with support for: - Webpack - ES2015 via Babel-Loader - Different supported style languages (sass, scss, less, stylus) +- Style transformations via PostCSS - Automatic code linting via esLint - Ability to unit test components via Karma and Mocha/Chai @@ -64,6 +65,27 @@ Stateless functional components where introduced in React v0.14. They have a muc ___Note___: You will still be able to set properties for stateless components! +## Adding PostCSS plugins +If you have enabled [PostCSS](https://github.com/postcss/postcss) at generation time, install your PostCSS plugins via npm and *require* it in **postcss** function in *cfg/base.js*. + +Example for autoprefixer: +```bash +cd my-new-project +npm install autoprefixer +``` +Require in *cfg/base.js* +```JavaScript +... +postcss: function () { + return [ + require('autoprefixer')({ + browsers: ['last 2 versions', 'ie >= 8'] + }) + ]; +} +... +``` + ## Usage The following commands are available in your project: ```bash diff --git a/generators/app/index.js b/generators/app/index.js index 5c19dd8..a1dc155 100644 --- a/generators/app/index.js +++ b/generators/app/index.js @@ -46,11 +46,15 @@ module.exports = generator.Base.extend({ // Set needed global vars for yo this.appName = props.appName; this.style = props.style; + this.postcss = props.postcss // Set needed keys into config this.config.set('appName', this.appName); this.config.set('appPath', this.appPath); this.config.set('style', this.style); + this.config.set('postcss', this.postcss); + + this.config.save(); done(); }.bind(this)); @@ -83,6 +87,15 @@ module.exports = generator.Base.extend({ } } + // Add postcss module if enabled + let postcssConfig = utils.config.getChoiceByKey('postcss', 'postcss'); + if(this.postcss && postcssConfig && postcssConfig.packages) { + + for(let dependency of postcssConfig.packages) { + packageSettings.dependencies[dependency.name] = dependency.version; + } + } + this.fs.writeJSON(this.destinationPath('package.json'), packageSettings); }, @@ -123,6 +136,11 @@ module.exports = generator.Base.extend({ }, install: function() { + if(this.postcss) { + let postcss = require('./postcss'); + postcss.write(path.join(this.destinationRoot(), 'cfg/base.js')); + } + if(!this.options['skip-install']) { this.installDependencies({ bower: false }); } diff --git a/generators/app/postcss.js b/generators/app/postcss.js new file mode 100644 index 0000000..9bf2377 --- /dev/null +++ b/generators/app/postcss.js @@ -0,0 +1,39 @@ +'use strict'; +let fs = require('fs'); +let esprima = require('esprima'); +let walk = require('esprima-walk'); +let escodegen = require('escodegen'); + +module.exports = { + write: function(path) { + let baseConfigPath = path; + let cssDialects = ['/\\.css$/', '/\\.sass/', '/\\.scss/', '/\\.less/', '/\\.styl/']; + + let postcss = 'var postcss = { postcss: function() { return []; } }'; + postcss = esprima.parse(postcss); + postcss = postcss.body[0].declarations[0].init.properties[0]; + + let data = fs.readFileSync(baseConfigPath, 'utf8'); + let parsed = esprima.parse(data); + + walk.walkAddParent(parsed, function(node) { + if(node.type === 'AssignmentExpression' && node.left.object.name === 'module') { + node.right.properties.push(postcss); + } + + if(node.type === 'Property' && node.key.name === 'test') { + if(cssDialects.indexOf(node.value.raw) > -1) { + let current = node.parent.properties[1].value.value; + let position = current.split('!', 2).join('!').length; + current = [current.slice(0, position), '!postcss-loader', current.slice(position)].join(''); + + node.parent.properties[1].value.value = current; + } + } + }); + + let options = { format: { indent: { style: ' ' } } }; + let code = escodegen.generate(parsed, options); + fs.writeFileSync(baseConfigPath, code, 'utf8'); + } +} diff --git a/generators/app/prompts.js b/generators/app/prompts.js index 5c447ea..77a90fe 100644 --- a/generators/app/prompts.js +++ b/generators/app/prompts.js @@ -14,5 +14,11 @@ module.exports = [ message: 'Which styles language you want to use?', choices: utils.config.getChoices('style'), default: utils.config.getDefaultChoice('style') + }, + { + type: 'confirm', + name: 'postcss', + message: 'Enable postcss?', + default: false } ]; diff --git a/package.json b/package.json index 4665756..daba21f 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,10 @@ "react-webpack-template": "^1.0.0", "underscore.string": "^3.2.2", "yeoman-generator": "^0.21.0", - "yeoman-welcome": "^1.0.1" + "yeoman-welcome": "^1.0.1", + "esprima": "^2.7.0" , + "esprima-walk": "^0.1.0" , + "escodegen": "^1.7.0" }, "devDependencies": { "chai": "^3.2.0", diff --git a/test/generators/app/indexTest.js b/test/generators/app/indexTest.js index 802e964..419070c 100644 --- a/test/generators/app/indexTest.js +++ b/test/generators/app/indexTest.js @@ -35,6 +35,10 @@ describe('react-webpack:app', () => { it('should use "css" as default style language', () => { expect(generator.config.get('style')).to.equal('css'); }); + + it('should not enable "PostCSS" by default', () => { + expect(generator.config.get('postcss')).to.equal(false); + }); }); describe('#createFiles', () => { @@ -95,3 +99,113 @@ describe('react-webpack:app', () => { }); }); }); + +describe('react-webpack:app non-default-prompts', () => { + + let defaultPrompts = require('../../../generators/app/prompts.js'); + let prompts = {}; + for(let p of defaultPrompts) { + prompts[p.name] = p.default; + } + + prompts.postcss = true; + + let generator; + let generatorBase = path.join(__dirname, '../../../generators/app'); + + before((done) => { + + helpers.run(generatorBase) + .inTmpDir() + .withOptions({ + 'skip-welcome-message': true, + 'skip-install': true + }) + .withPrompts(prompts) + .on('ready', (instance) => { + generator = instance; + }) + .on('end', done); + }); + + describe('#config', () => { + + it('should use "css" as default style language', () => { + expect(generator.config.get('style')).to.equal('css'); + }); + }); + + describe('#createFiles', () => { + + it('should generate dot files', () => { + + assert.file([ + '.editorconfig', + '.eslintrc', + '.gitignore', + '.npmignore', + '.yo-rc.json' + ]); + }); + + it('should generate project configuration files', () => { + + assert.file([ + 'LICENSE', + 'package.json' + ]); + }); + + it('should generate the webpack configuration', () => { + + assert.file([ + 'cfg/base.js', + 'cfg/dev.js', + 'cfg/dist.js', + 'cfg/test.js', + 'server.js', + 'webpack.config.js' + ]); + }); + + it('should insert the postcss loader into the style pipes', () => { + + assert.fileContent('cfg/base.js', 'loader: \'style!css!postcss-loader\''); + //assert.fileContent('cfg/base.js', 'loader: \'style-loader!css-loader!postcss-loader\''); + assert.fileContent('cfg/base.js', 'loader: \'style-loader!css-loader!postcss-loader!sass-loader?outputStyle=expanded&indentedSyntax\''); + assert.fileContent('cfg/base.js', 'loader: \'style-loader!css-loader!postcss-loader!sass-loader?outputStyle=expanded\''); + assert.fileContent('cfg/base.js', 'loader: \'style-loader!css-loader!postcss-loader!less-loader\''); + assert.fileContent('cfg/base.js', 'loader: \'style-loader!css-loader!postcss-loader!stylus-loader\''); + }); + + it('should append the postcss function to the base config', () => { + + assert.fileContent('cfg/base.js', ',\n postcss: function () {\n return [];\n }'); + }); + + it('should generate required source files', () => { + + assert.file([ + 'src/actions/README.md', + 'src/components/Main.js', + 'src/components/run.js', + 'src/favicon.ico', + 'src/images/yeoman.png', + 'src/index.html', + 'src/sources/README.md', + 'src/stores/README.md', + 'src/styles/App.css' + ]); + }); + + it('should generate test configuration and basic tests', () => { + + assert.file([ + 'karma.conf.js', + 'test/components/MainTest.js', + 'test/helpers/shallowRenderHelper.js', + 'test/loadtests.js' + ]); + }); + }); +}); diff --git a/utils/configopts.json b/utils/configopts.json index 2a157ba..8012f56 100644 --- a/utils/configopts.json +++ b/utils/configopts.json @@ -39,6 +39,18 @@ } ] }, + "postcss": { + "options": [ + { + "name": "postcss", + "value": "postcss", + "packages": [ + { "name": "postcss", "version": "^5.0.11" }, + { "name": "postcss-loader", "version": "^0.8.0" } + ] + } + ] + }, "style": { "options": [ {