Added sub-generator setup-env #278 (#308)

This subgen creates a new Webpack config environment by

- creating conf/webpack/<EnvName>.js
- creating src/config/<env_name>.js
- requiring and exporting the new env in conf/webpack/index.js

The commit introduces a basic config template that is supposed to be populated by the generator's users. Various more fine grained subgen options can be added at a later time (e.g. prompting the user if new run scripts should be added to package.json).

The subgen's basic functionality is backed up by unit tests that check

- if files are created
- if conf/webpack/index.js contains correct import/export
This commit is contained in:
Stephan Herzog 2016-11-22 19:38:13 +01:00 committed by GitHub
parent a2bd75a83b
commit 01855bd2ed
8 changed files with 290 additions and 1 deletions

View File

@ -0,0 +1,22 @@
'use strict';
const esDefaultOpts = require('esformatter/lib/preset/default.json');
const esOpts = Object.assign({}, esDefaultOpts, {
'lineBreak': {
'before': {
'AssignmentExpression': '>=2',
'ClassDeclaration': 2,
'EndOfFile': 1
},
'after': {
'ClassClosingBrace': 2,
'FunctionDeclaration': '>=2',
'BlockStatementClosingBrace': '>=2'
}
}
});
module.exports = {
esOpts
};

View File

@ -0,0 +1,61 @@
'use strict';
const Generators = require('yeoman-generator');
const classify = require('underscore.string').classify;
const underscored = require('underscore.string').underscored;
const formatCode = require('./utils').formatCode;
const getModifiedConfigModuleIndex = require('./utils').getModifiedConfigModuleIndex;
class EnvGenerator extends Generators.Base {
constructor(args, options) {
super(args, options);
this.argument('envName', { type: String, required: true });
}
configuring() {
/**
* Currently used major version of the generator (defaults to latest stable).
* @type {number}
*/
this.generatorVersion = this.config.get('generatedWithVersion') || 3;
// Make sure we don't try to use this subgen on V3 or lower.
if (this.generatorVersion < 4) {
this.env.error('Setting up new envs is only supported in generator versions 4+');
}
}
writing() {
const classedEnv = classify(this.envName);
const snakedEnv = underscored(this.envName.toLowerCase());
// Write conf/webpack/<EnvName>.js
this.fs.copyTpl(
this.templatePath(`${this.generatorVersion}/Env.js`),
this.destinationPath(`conf/webpack/${classedEnv}.js`),
{ envName: snakedEnv }
);
// Write src/config/<env_name>.js
this.fs.copyTpl(
this.templatePath(`${this.generatorVersion}/runtimeConfig.js`),
this.destinationPath(`src/config/${snakedEnv}.js`),
{ envName: snakedEnv }
);
// Write conf/webpack/index.js
const moduleIndexPath = this.destinationPath('conf/webpack/index.js');
const updatedModuleIndex = formatCode(
getModifiedConfigModuleIndex(this.fs.read(moduleIndexPath), snakedEnv, classedEnv)
);
this.fs.write(this.destinationPath('conf/webpack/index.js'), formatCode(updatedModuleIndex));
}
}
module.exports = EnvGenerator;

View File

@ -0,0 +1,29 @@
'use strict';
/**
* Default dev server configuration.
*/
const webpack = require('webpack');
const WebpackBaseConfig = require('./Base');
class WebpackDevConfig extends WebpackBaseConfig {
constructor() {
super();
this.config = {
// Update your env-specific configuration here!
// To start, look at ./Dev.js or ./Dist.js for two example configurations
// targeted at production or development builds.
};
}
/**
* Get the environment name
* @return {String} The current environment
*/
get env() {
return '<%= envName %>';
}
}
module.exports = WebpackDevConfig;

View File

@ -0,0 +1,7 @@
import baseConfig from './base';
const config = {
appEnv: '<%= envName %>',
};
export default Object.freeze(Object.assign({}, baseConfig, config));

View File

@ -0,0 +1,82 @@
'use strict';
const acorn = require('acorn');
const escodegen = require('escodegen');
const esformatter = require('esformatter');
const jp = require('jsonpath');
const esOpts = require('./constants').esOpts;
/**
* Returns an AST Node for a {@code Property} in the {@code module.exports} object.
*
* @param {string} envName
* @return {Object}
*/
function createExportNode(envName) {
return {
'type': 'Property',
'method': false,
'shorthand': true,
'computed': false,
'key': {
'type': 'Identifier',
'name': envName
},
'kind': 'init',
'value': {
'type': 'Identifier',
'name': envName
}
}
}
/**
* Returns updated index module requiring and exporting the newly created environment.
*
* @param {string} fileStr
* @param {string} snakedEnv
* @param {string} classedEnv
* @return {string} file contents of updated conf/webpack/index.js
*/
function getModifiedConfigModuleIndex(fileStr, snakedEnv, classedEnv) {
// TODO [sthzg] we might want to rewrite the AST-mods in this function using a walker.
const moduleFileAst = acorn.parse(fileStr, { module: true });
// if required env was already created, just return the original string
if (jp.paths(moduleFileAst, `$..[?(@.value=="./${classedEnv}" && @.type=="Literal")]`).length > 0) {
return fileStr;
}
// insert require call for the new env
const envImportAst = acorn.parse(`const ${snakedEnv} = require('./${classedEnv}');`);
const insertAt = jp.paths(moduleFileAst, '$..[?(@.name=="require")]').pop()[2] + 1;
moduleFileAst.body.splice(insertAt, 0, envImportAst);
// add new env to module.exports
const exportsAt = jp.paths(moduleFileAst, '$..[?(@.name=="exports")]').pop()[2];
moduleFileAst.body[exportsAt].expression.right.properties.push(createExportNode(snakedEnv));
return escodegen.generate(moduleFileAst, { format: { indent: { style: ' ' } } });
}
/**
* Returns a beautified representation of {@code fileStr}.
*
* @param {string} fileStr
* @return {string}
*/
function formatCode(fileStr) {
return esformatter.format(fileStr, esOpts);
}
module.exports = {
createExportNode,
formatCode,
getModifiedConfigModuleIndex
};

View File

@ -47,9 +47,12 @@
"release:patch": "npm version prerelease && git push --follow-tags && npm publish --tag beta"
},
"dependencies": {
"escodegen": "^1.7.0",
"acorn": "^4.0.3",
"escodegen": "^1.8.1",
"esformatter": "^0.9.6",
"esprima": "^3.1.1",
"esprima-walk": "^0.1.0",
"jsonpath": "^0.2.7",
"react-webpack-template": "^2.0.1-5",
"underscore.string": "^3.2.2",
"yeoman-generator": "^0.24.0",
@ -58,6 +61,7 @@
"devDependencies": {
"chai": "^3.2.0",
"coveralls": "^2.11.12",
"fs-extra": "^0.30.0",
"istanbul": "^0.4.5",
"mocha": "^3.0.0",
"yeoman-assert": "^2.1.1",

View File

@ -0,0 +1,7 @@
'use strict';
const foo = require('path');
module.exports = {
foo
};

View File

@ -0,0 +1,77 @@
'use strict';
const acorn = require('acorn');
const assert = require('yeoman-assert');
const fs = require('fs-extra');
const helpers = require('yeoman-test');
const path = require('path');
const walk = require('acorn/dist/walk');
/**
* Returns absolute path to (sub-)generator with {@code name}.
* @param {string} name
*/
const genpath = (name) =>
path.join(__dirname, '../../../generators', name);
/**
* A mocked generator config object.
* @type {{appName: string, style: string, cssmodules: boolean, postcss: boolean, generatedWithVersion: number}}
*/
const cfg = {
appName: 'testCfg',
style: 'css',
cssmodules: false,
postcss: false,
generatedWithVersion: 4
};
describe('react-webpack:setup-env', function () {
describe('react-webpack:setup-env foobar', function () {
before(function () {
return helpers
.run(genpath('setup-env'))
.withArguments(['foobar'])
.withLocalConfig(cfg)
.inTmpDir(function (dir) {
fs.copySync(
path.join(__dirname, 'assets/moduleIndex.js'),
path.join(dir, 'conf/webpack/index.js')
);
})
.toPromise();
});
it('creates env files', function () {
assert.file(['conf/webpack/Foobar.js']);
assert.file(['src/config/foobar.js']);
});
it('requires the new env in conf/webpack/index.js', function () {
assert.fileContent(
'conf/webpack/index.js',
/const foobar = require\('\.\/Foobar'\)/
);
});
it('exports the new env from conf/webpack/index.js', function () {
const fileStr = fs.readFileSync('conf/webpack/index.js').toString();
const ast = acorn.parse(fileStr);
let found = false;
walk.simple(ast, {
'Property': (node) => {
if (node.key.name === 'foobar' && node.value.name === 'foobar') {
found = true;
}
}
});
assert(found, 'Did not find a key and value of `foobar` on the module.exports AST node');
});
});
});