Refactored component subgen to support styles and/or cssmodules #255

This commit refactors the template structure for components to multiple
different base templates based on the required styling options:

- nostyle
- with styles
- with styles and css modules
This commit is contained in:
sthzg 2016-08-03 15:38:39 +02:00
parent 389cc94bf4
commit e2add4af4d
18 changed files with 299 additions and 92 deletions

View File

@ -1,12 +1,39 @@
'use strict';
const Generators = require('yeoman-generator');
let utils = require('../../utils/all');
const utils = require('../../utils/all');
const C = utils.constants;
const getAllSettingsFromComponentName = utils.yeoman.getAllSettingsFromComponentName;
class ComponentGenerator extends Generators.Base {
constructor(args, options) {
super(args, options);
/**
* Flag indicating whether the component should be created with associated style files.
* @type {boolean}
*/
this.useStyles = false;
/**
* Flag indicating whether the component should make use of css modules.
* @type {boolean}
*/
this.useCssModules = false;
/**
* Filename of the template that will be used to create the component.
* @type {?string}
*/
this.componentTemplateName = null;
/**
* Generator and template version to create the component from.
* @type {?number}
*/
this.generatorVersion = null;
this.argument('name', { type: String, required: true });
this.option('stateless', {
@ -20,27 +47,37 @@ class ComponentGenerator extends Generators.Base {
});
}
writing() {
// Set the template base. If it cannot be guessed,
// use files from the default directory. In this case,
// assume we have something REALLY REALLY old here...
let generatorVersion = this.config.get('generatedWithVersion');
if(!generatorVersion) {
generatorVersion = 3;
configuring() {
// Read the requested major version or default it to the latest stable
this.generatorVersion = this.config.get('generatedWithVersion') || 3;
if (!C.SUPPORTED_GEN_VERSIONS.some(x => x === this.generatorVersion)) {
this.env.error('Unsupported generator version');
}
const componentType = this.options.stateless ? 'Stateless' : 'Base';
const componentHasStyles = !this.options.nostyle;
this.useStyles = !this.options.nostyle;
this.useCssModules = this.config.get('cssmodules') || false;
// Get the
const settings = utils.yeoman.getAllSettingsFromComponentName(this.name, this.config.get('style'), generatorVersion);
settings.componentHasStyles = componentHasStyles;
// Make sure we don't try to use template v3 with cssmodules
if (this.generatorVersion < 4 && this.useStyles && this.useCssModules) {
this.env.error('Creating components with cssmodules is only supported in generator versions 4+');
}
// Get the filename of the component template to be copied during this run
this.componentTemplateName =
utils.yeoman.getComponentTemplateName(this.options.stateless, this.useStyles, this.useCssModules);
}
writing() {
const settings =
getAllSettingsFromComponentName(this.name, this.config.get('style'), this.useCssModules, this.generatorVersion);
// Create the style template. Skipped if nostyle is set as command line flag
if(componentHasStyles) {
if(this.useStyles) {
this.fs.copyTpl(
this.templatePath(`${generatorVersion}/styles/Component${settings.style.suffix}`),
this.templatePath(`${this.generatorVersion}/styles/Component${settings.style.suffix}`),
this.destinationPath(settings.style.path + settings.style.fileName),
settings
);
@ -48,14 +85,14 @@ class ComponentGenerator extends Generators.Base {
// Create the component
this.fs.copyTpl(
this.templatePath(`${generatorVersion}/components/${componentType}.js`),
this.templatePath(`${this.generatorVersion}/components/${this.componentTemplateName}`),
this.destinationPath(settings.component.path + settings.component.fileName),
settings
);
// Create the unit test
this.fs.copyTpl(
this.templatePath(`${generatorVersion}/tests/Base.js`),
this.templatePath(`${this.generatorVersion}/tests/Base.js`),
this.destinationPath(settings.test.path + settings.test.fileName),
settings
);

View File

@ -0,0 +1,21 @@
'use strict';
import React from 'react';
class <%= component.className %> extends React.Component {
render() {
return (
<div className="<%= style.className %>">
Please edit <%= component.path %>/<%= component.fileName %> to update this component!
</div>
);
}
}
<%= component.className %>.displayName = '<%= component.displayName %>';
// Uncomment properties you need
// <%= component.className %>.propTypes = {};
// <%= component.className %>.defaultProps = {};
export default <%= component.className %>;

View File

@ -0,0 +1,17 @@
'use strict';
import React from 'react';
let <%= component.className %> = () => (
<div className="<%= style.className %>">
Please edit <%= component.path %>/<%= component.fileName %> to update this component!
</div>
);
<%= component.className %>.displayName = '<%= component.displayName %>';
// Uncomment properties you need
// <%= component.className %>.propTypes = {};
// <%= component.className %>.defaultProps = {};
export default <%= component.className %>;

View File

@ -4,7 +4,7 @@ import React from 'react';
require('<%= style.webpackPath %>');
let <%= component.className %> = (props) => (
let <%= component.className %> = () => (
<div className="<%= style.className %>">
Please edit <%= component.path %>/<%= component.fileName %> to update this component!
</div>

View File

@ -1,9 +1,8 @@
import React from 'react';<% if(componentHasStyles) { %>
import React from 'react';
import cssmodules from 'react-css-modules';
import styles from '<%= style.webpackPath %>';<% } %>
import styles from '<%= style.webpackPath %>';
<% if(componentHasStyles) { %>@cssmodules(styles)
<% } %>class <%= component.className %> extends React.Component {
class <%= component.className %> extends React.Component {
render() {
return (
@ -18,4 +17,4 @@ import styles from '<%= style.webpackPath %>';<% } %>
<%= component.className %>.propTypes = {};
<%= component.className %>.defaultProps = {};
export default <%= component.className %>;
export default cssmodules(<%= component.className %>, styles);

View File

@ -0,0 +1,18 @@
import React from 'react';
class <%= component.className %> extends React.Component {
render() {
return (
<div className="<%= style.className %>">
Please edit <%= component.path %><%= component.fileName %> to update this component!
</div>
);
}
}
<%= component.className %>.displayName = '<%= component.displayName %>';
<%= component.className %>.propTypes = {};
<%= component.className %>.defaultProps = {};
export default <%= component.className %>;

View File

@ -0,0 +1,19 @@
import React from 'react';
import '<%= style.webpackPath %>';
class <%= component.className %> extends React.Component {
render() {
return (
<div className="<%= style.className %>">
Please edit <%= component.path %><%= component.fileName %> to update this component!
</div>
);
}
}
<%= component.className %>.displayName = '<%= component.displayName %>';
<%= component.className %>.propTypes = {};
<%= component.className %>.defaultProps = {};
export default <%= component.className %>;

View File

@ -1,20 +0,0 @@
import React from 'react';<% if(componentHasStyles) { %>
import cssmodules from 'react-css-modules';
import styles from '<%= style.webpackPath %>';<% } %>
const <%= component.className %> = () => {
return (
<div className="<%= style.className %>" styleName="<%= style.className %>">
Please edit <%= component.path %><%= component.fileName %> to update this component!
</div>
);
};
<%= component.className %>.displayName = '<%= component.displayName %>';
<%= component.className %>.propTypes = {};
<%= component.className %>.defaultProps = {};
<% if(componentHasStyles) { %>export default cssmodules(<%= component.className %>, styles);<%
} else {
%>export default <%= component.className %>;<% } %>

View File

@ -0,0 +1,15 @@
import React from 'react';
import cssmodules from 'react-css-modules';
import styles from '<%= style.webpackPath %>';
const <%= component.className %> = () => (
<div className="<%= style.className %>" styleName="<%= style.className %>">
Please edit <%= component.path %><%= component.fileName %> to update this component!
</div>
);
<%= component.className %>.displayName = '<%= component.displayName %>';
<%= component.className %>.propTypes = {};
<%= component.className %>.defaultProps = {};
export default cssmodules(<%= component.className %>, styles);

View File

@ -0,0 +1,13 @@
import React from 'react';
const <%= component.className %> = () => (
<div className="<%= style.className %>">
Please edit <%= component.path %><%= component.fileName %> to update this component!
</div>
);
<%= component.className %>.displayName = '<%= component.displayName %>';
<%= component.className %>.propTypes = {};
<%= component.className %>.defaultProps = {};
export default <%= component.className %>;

View File

@ -0,0 +1,14 @@
import React from 'react';
import '<%= style.webpackPath %>';
const <%= component.className %> = () => (
<div className="<%= style.className %>">
Please edit <%= component.path %><%= component.fileName %> to update this component!
</div>
);
<%= component.className %>.displayName = '<%= component.displayName %>';
<%= component.className %>.propTypes = {};
<%= component.className %>.defaultProps = {};
export default <%= component.className %>;

View File

@ -98,7 +98,7 @@ describe('react-webpack:app', () => {
'src/actions/README.md',
'src/index.js',
'src/components/App.js',
'src/components/app.cssmodule.css',
'src/components/app.css',
'src/favicon.ico',
'src/images/yeoman.png',
'src/index.html',

View File

@ -3,6 +3,7 @@ let path = require('path');
let assert = require('yeoman-assert');
let helpers = require('yeoman-test');
describe('react-webpack:component', () => {
const generatorComponent = path.join(__dirname, '../../../generators/component');
@ -182,70 +183,78 @@ describe('react-webpack:component', () => {
*/
let generator;
const cssModSuffix = (useCssModules) => useCssModules ? '.cssmodule' : '';
const importAssertion = (useCssModules, ext) => useCssModules
? `import styles from './mycomponent.cssmodule.${ext}';`
: `import './mycomponent.${ext}';`
;
// List of available style types. Please add a line that says
// testComponentWithStyle(styleTypes.KEY); to the bottom of the file
// to run all unit tests for this filetype.
const styleTypes = {
const styleTypes = (useCssModules) => ({
css: {
type: 'css',
fileName: 'src/components/mycomponent.cssmodule.css',
expandedFileName: 'src/components/my/littleSpecial/test.cssmodule.css',
fileName: `src/components/mycomponent${cssModSuffix(useCssModules)}.css`,
expandedFileName: `src/components/my/littleSpecial/test${cssModSuffix(useCssModules)}.css`,
assertions: {
componentImport: 'import styles from \'./mycomponent.cssmodule.css\';',
componentImport: importAssertion(useCssModules, 'css'),
styleContent: '.mycomponent-component'
}
},
sass: {
type: 'sass',
fileName: 'src/components/mycomponent.cssmodule.sass',
expandedFileName: 'src/components/my/littleSpecial/test.cssmodule.sass',
fileName: `src/components/mycomponent${cssModSuffix(useCssModules)}.sass`,
expandedFileName: `src/components/my/littleSpecial/test${cssModSuffix(useCssModules)}.sass`,
assertions: {
componentImport: 'import styles from \'./mycomponent.cssmodule.sass\';',
componentImport: importAssertion(useCssModules, 'sass'),
styleContent: '.mycomponent-component'
}
},
scss: {
type: 'scss',
fileName: 'src/components/mycomponent.cssmodule.scss',
expandedFileName: 'src/components/my/littleSpecial/test.cssmodule.scss',
fileName: `src/components/mycomponent${cssModSuffix(useCssModules)}.scss`,
expandedFileName: `src/components/my/littleSpecial/test${cssModSuffix(useCssModules)}.scss`,
assertions: {
componentImport: 'import styles from \'./mycomponent.cssmodule.scss\';',
componentImport: importAssertion(useCssModules, 'scss'),
styleContent: '.mycomponent-component'
}
},
less: {
type: 'less',
fileName: 'src/components/mycomponent.cssmodule.less',
expandedFileName: 'src/components/my/littleSpecial/test.cssmodule.less',
fileName: `src/components/mycomponent${cssModSuffix(useCssModules)}.less`,
expandedFileName: `src/components/my/littleSpecial/test${cssModSuffix(useCssModules)}.less`,
assertions: {
componentImport: 'import styles from \'./mycomponent.cssmodule.less\';',
componentImport: importAssertion(useCssModules, 'less'),
styleContent: '.mycomponent-component'
}
},
stylus: {
type: 'stylus',
fileName: 'src/components/mycomponent.cssmodule.styl',
expandedFileName: 'src/components/my/littleSpecial/test.cssmodule.styl',
fileName: `src/components/mycomponent${cssModSuffix(useCssModules)}.styl`,
expandedFileName: `src/components/my/littleSpecial/test${cssModSuffix(useCssModules)}.styl`,
assertions: {
componentImport: 'import styles from \'./mycomponent.cssmodule.styl\';',
componentImport: importAssertion(useCssModules, 'styl'),
styleContent: '.mycomponent-component'
}
}
};
});
/**
* Return a newly generated component with given name and style
* @param {String} name Name of the component
* @param {String} styleType Styling language to use
* @param {Object} options Options to use for the generator
* @param {boolean} useCssModules useCssModules indicate whether to test with cssmodules enabled
* @param {Function} callback Test callback to run
*/
function createGeneratedComponent(name, styleType, options, callback) {
function createGeneratedComponent(name, styleType, options, useCssModules, callback) {
helpers.run(generatorComponent)
.withArguments([name])
.withOptions(options)
.on('ready', (instance) => {
instance.config.set('style', styleType);
instance.config.set('cssmodules', useCssModules);
instance.config.set('generatedWithVersion', 4);
generator = instance;
})
@ -256,20 +265,21 @@ describe('react-webpack:component', () => {
* Test a component with styling applied
* @param {Object} style The style to apply (see styleTypes above)
* @param {Object} options Options to use [optional]
* @param {boolean} useCssModules indicate whether to test with cssmodules enabled
*/
function testComponentWithStyle(style, options) {
function testComponentWithStyle(style, options, useCssModules) {
// Make sure we always have options
if(!options) {
options = {};
}
describe(`when using style type "${style.type}" including with nostyle set to false`, () => {
describe(`when using style type "${style.type}" with nostyle = false and cssmodules = ${useCssModules}`, () => {
describe('when writing is called', () => {
it(`should create the react component, its ${style.type}-stylesheet and test file`, (done) => {
createGeneratedComponent('mycomponent', style.type, options, () => {
createGeneratedComponent('mycomponent', style.type, options, useCssModules, () => {
assert.file([
'src/components/Mycomponent.js',
@ -284,34 +294,34 @@ describe('react-webpack:component', () => {
describe('when creating a component', () => {
it('should always import REACT', (done) => {
createGeneratedComponent('mycomponent', style.type, options, () => {
createGeneratedComponent('mycomponent', style.type, options, useCssModules, () => {
assert.fileContent('src/components/Mycomponent.js', 'import React from \'react\';');
done();
});
});
it(`should require the created ${style.type} file`, (done) => {
createGeneratedComponent('mycomponent', style.type, options, () => {
createGeneratedComponent('mycomponent', style.type, options, useCssModules, () => {
assert.fileContent('src/components/Mycomponent.js', style.assertions.componentImport);
done();
});
});
it('should have its displayName set per default', (done) => {
createGeneratedComponent('mycomponent', style.type, options, () => {
createGeneratedComponent('mycomponent', style.type, options, useCssModules, () => {
assert.fileContent('src/components/Mycomponent.js', 'Mycomponent.displayName = \'Mycomponent\';');
done();
});
});
it('should export the created component', (done) => {
createGeneratedComponent('mycomponent', style.type, options, () => {
createGeneratedComponent('mycomponent', style.type, options, useCssModules, () => {
let exportAssertion;
if(generator.options.stateless) {
if(useCssModules) {
exportAssertion = 'export default cssmodules(Mycomponent, styles);';
} else {
exportAssertion = 'export default Mycomponent';
exportAssertion = 'export default Mycomponent;';
}
assert.fileContent('src/components/Mycomponent.js', exportAssertion);
done();
@ -319,7 +329,7 @@ describe('react-webpack:component', () => {
});
it('should be possible to create components in a subfolder', (done) => {
createGeneratedComponent('my/little !special/test', style.type, options, () => {
createGeneratedComponent('my/little !special/test', style.type, options, useCssModules, () => {
assert.file([
'src/components/my/littleSpecial/Test.js',
@ -331,14 +341,14 @@ describe('react-webpack:component', () => {
});
it(`should add the components ${style.type} class to the created stylesheet`, (done) => {
createGeneratedComponent('mycomponent', style.type, options, () => {
createGeneratedComponent('mycomponent', style.type, options, useCssModules, () => {
assert.fileContent(style.fileName, style.assertions.styleContent);
done();
});
});
it('should create a unit test that imports the generated component', (done) => {
createGeneratedComponent('mycomponent', style.type, options, () => {
createGeneratedComponent('mycomponent', style.type, options, useCssModules, () => {
assert.fileContent('test/components/MycomponentTest.js', 'import Mycomponent from \'components/Mycomponent.js\';');
done();
});
@ -351,20 +361,21 @@ describe('react-webpack:component', () => {
* Test a component with styling applied
* @param {Object} style The style to apply (see styleTypes above)
* @param {Object} options Options to use [optional]
* @param {boolean} useCssModules indicate whether to test with cssmodules enabled
*/
function testComponentWithoutStyle(style, options) {
function testComponentWithoutStyle(style, options, useCssModules) {
// Make sure we always have options
if(!options) {
options = {};
}
describe(`when using style type "${style.type}" with nostyle set to true`, () => {
describe(`when using style type "${style.type}" with nostyle = true and cssmodules = ${useCssModules}`, () => {
describe('when writing is called', () => {
it('should create the react component, and test file', (done) => {
createGeneratedComponent('mycomponent', style.type, options, () => {
createGeneratedComponent('mycomponent', style.type, options, useCssModules, () => {
assert.file([
'src/components/Mycomponent.js',
@ -378,28 +389,28 @@ describe('react-webpack:component', () => {
describe('when creating a component', () => {
it('should always import REACT', (done) => {
createGeneratedComponent('mycomponent', style.type, options, () => {
createGeneratedComponent('mycomponent', style.type, options, useCssModules, () => {
assert.fileContent('src/components/Mycomponent.js', 'import React from \'react\';');
done();
});
});
it('should have its displayName set per default', (done) => {
createGeneratedComponent('mycomponent', style.type, options, () => {
createGeneratedComponent('mycomponent', style.type, options, useCssModules, () => {
assert.fileContent('src/components/Mycomponent.js', 'Mycomponent.displayName = \'Mycomponent\';');
done();
});
});
it('should export the created component', (done) => {
createGeneratedComponent('mycomponent', style.type, options, () => {
createGeneratedComponent('mycomponent', style.type, options, useCssModules, () => {
assert.fileContent('src/components/Mycomponent.js', 'export default Mycomponent');
done();
});
});
it('should be possible to create components in a subfolder', (done) => {
createGeneratedComponent('my/little !special/test', style.type, options, () => {
createGeneratedComponent('my/little !special/test', style.type, options, useCssModules, () => {
assert.file([
'src/components/my/littleSpecial/Test.js',
@ -410,7 +421,7 @@ describe('react-webpack:component', () => {
});
it('should create a unit test that imports the generated component', (done) => {
createGeneratedComponent('mycomponent', style.type, options, () => {
createGeneratedComponent('mycomponent', style.type, options, useCssModules, () => {
assert.fileContent('test/components/MycomponentTest.js', 'import Mycomponent from \'components/Mycomponent.js\';');
done();
});
@ -421,10 +432,15 @@ describe('react-webpack:component', () => {
// Run all tests for all available style types.
// Stateless components will also be tested!
for(const style in styleTypes) {
testComponentWithStyle(styleTypes[style]);
testComponentWithStyle(styleTypes[style], { stateless: true });
testComponentWithoutStyle(styleTypes[style], { nostyle: true });
for(const style in styleTypes(true)) {
testComponentWithStyle(styleTypes(true)[style], {}, true);
testComponentWithStyle(styleTypes(true)[style], { stateless: true }, true);
testComponentWithoutStyle(styleTypes(true)[style], { nostyle: true }, true);
}
for(const style in styleTypes(false)) {
testComponentWithStyle(styleTypes(false)[style], {}, false);
testComponentWithStyle(styleTypes(false)[style], { stateless: true }, false);
testComponentWithoutStyle(styleTypes(false)[style], { nostyle: true }, false);
}
});
});

View File

@ -114,11 +114,11 @@ describe('Utilities:Yeoman', () => {
};
it('should get all required information for component creation from the components name', () => {
expect(utils.getAllSettingsFromComponentName('my/component/test', 'css', 4)).to.deep.equal(expectionNamespaced);
expect(utils.getAllSettingsFromComponentName('my/component/test', 'css', true, 4)).to.deep.equal(expectionNamespaced);
});
it('should build path information wo/ two slashes when dealing with a non-namespaced component', () => {
expect(utils.getAllSettingsFromComponentName('test', 'css', 4)).to.deep.equal(expectionRoot);
expect(utils.getAllSettingsFromComponentName('test', 'css', true, 4)).to.deep.equal(expectionRoot);
});

View File

@ -1,9 +1,11 @@
'use strict';
const config = require('./config');
const constants = require('./constants');
const yeoman = require('./yeoman');
module.exports = {
config,
constants,
yeoman
};

35
utils/constants.js Normal file
View File

@ -0,0 +1,35 @@
'use strict';
/**
* List of supported generator versions.
* @type {number[]}
*/
const SUPPORTED_GEN_VERSIONS = [3, 4];
/**
* ENUM of supported component types.
* @type {{STATEFUL: string, STATELESS: string}}
*/
const COMP_TYPES = {
STATEFUL: 'Stateful',
STATELESS: 'Stateless'
};
/**
* ENUM of supported style types.
* @type {{WITH_STYLES: string, WITH_CSSMODULES: string, NO_STYLES: string}}
*/
const STYLE_TYPES = {
WITH_STYLES: 'WithStyles',
WITH_CSSMODULES: 'CssModules',
NO_STYLES: 'NoStyles'
};
module.exports = {
SUPPORTED_GEN_VERSIONS,
COMP_TYPES,
STYLE_TYPES
};

View File

@ -3,6 +3,7 @@
const path = require('path');
const configUtils = require('./config');
const _ = require('underscore.string');
const C = require('./constants');
// Needed directory paths
const baseName = path.basename(process.cwd());
@ -22,7 +23,7 @@ let getBaseDir = () => {
* @param {String|Number} generatorVersion The version of the generator [optional]
* @return {Object} Component settings
*/
let getAllSettingsFromComponentName = (componentName, style, generatorVersion) => {
let getAllSettingsFromComponentName = (componentName, style, useCssModules, generatorVersion) => {
// Use css per default
if(!style) {
@ -60,9 +61,9 @@ let getAllSettingsFromComponentName = (componentName, style, generatorVersion) =
case 4:
settings = {
style: {
webpackPath: `./${componentBaseName.toLowerCase()}.cssmodule${styleSettings.suffix}`,
webpackPath: `./${componentBaseName.toLowerCase()}${useCssModules ? '.cssmodule' : ''}${styleSettings.suffix}`,
path: path.normalize(`${componentPath.path}/${componentPartPath}/`),
fileName: `${componentBaseName.toLowerCase()}.cssmodule${styleSettings.suffix}`,
fileName: `${componentBaseName.toLowerCase()}${useCssModules ? '.cssmodule' : ''}${styleSettings.suffix}`,
className: getComponentStyleName(componentBaseName),
suffix: styleSettings.suffix
},
@ -197,12 +198,32 @@ let getDestinationClassName = (name, type, suffix) => {
return _.capitalize(fixedName.split('/').pop().split('.js')[0]);
};
/**
* Get the filename of the component template to copy.
* @param {boolean} isStateless
* @param {boolean} useStyles
* @param {boolean} useCssModules
* @return {string} The template filename including the .js suffix
*/
let getComponentTemplateName = (isStateless, useStyles, useCssModules) => {
const componentTypeFrag = isStateless ? C.COMP_TYPES.STATELESS : C.COMP_TYPES.STATEFUL;
const styleTypeFrag = !useStyles
? C.STYLE_TYPES.NO_STYLES
: useCssModules
? C.STYLE_TYPES.WITH_CSSMODULES
: C.STYLE_TYPES.WITH_STYLES
;
return `${componentTypeFrag}${styleTypeFrag}.js`;
};
module.exports = {
getBaseDir,
getAllSettingsFromComponentName,
getAppName,
getCleanedPathName,
getComponentStyleName,
getComponentTemplateName,
getDestinationPath,
getDestinationClassName
};