From 01855bd2edafab05586d4c5f5d64ebcdc81687c9 Mon Sep 17 00:00:00 2001 From: Stephan Herzog Date: Tue, 22 Nov 2016 19:38:13 +0100 Subject: [PATCH] Added sub-generator setup-env #278 (#308) This subgen creates a new Webpack config environment by - creating conf/webpack/.js - creating src/config/.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 --- generators/setup-env/constants.js | 22 +++++ generators/setup-env/index.js | 61 ++++++++++++++ generators/setup-env/templates/4/Env.js | 29 +++++++ .../setup-env/templates/4/runtimeConfig.js | 7 ++ generators/setup-env/utils.js | 82 +++++++++++++++++++ package.json | 6 +- .../setup-env/assets/moduleIndex.js | 7 ++ test/generators/setup-env/setupEnvTest.js | 77 +++++++++++++++++ 8 files changed, 290 insertions(+), 1 deletion(-) create mode 100644 generators/setup-env/constants.js create mode 100644 generators/setup-env/index.js create mode 100644 generators/setup-env/templates/4/Env.js create mode 100644 generators/setup-env/templates/4/runtimeConfig.js create mode 100644 generators/setup-env/utils.js create mode 100644 test/generators/setup-env/assets/moduleIndex.js create mode 100644 test/generators/setup-env/setupEnvTest.js diff --git a/generators/setup-env/constants.js b/generators/setup-env/constants.js new file mode 100644 index 0000000..ed4b13a --- /dev/null +++ b/generators/setup-env/constants.js @@ -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 +}; diff --git a/generators/setup-env/index.js b/generators/setup-env/index.js new file mode 100644 index 0000000..d4e2385 --- /dev/null +++ b/generators/setup-env/index.js @@ -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/.js + this.fs.copyTpl( + this.templatePath(`${this.generatorVersion}/Env.js`), + this.destinationPath(`conf/webpack/${classedEnv}.js`), + { envName: snakedEnv } + ); + + // Write src/config/.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; diff --git a/generators/setup-env/templates/4/Env.js b/generators/setup-env/templates/4/Env.js new file mode 100644 index 0000000..42e92c6 --- /dev/null +++ b/generators/setup-env/templates/4/Env.js @@ -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; diff --git a/generators/setup-env/templates/4/runtimeConfig.js b/generators/setup-env/templates/4/runtimeConfig.js new file mode 100644 index 0000000..120805f --- /dev/null +++ b/generators/setup-env/templates/4/runtimeConfig.js @@ -0,0 +1,7 @@ +import baseConfig from './base'; + +const config = { + appEnv: '<%= envName %>', +}; + +export default Object.freeze(Object.assign({}, baseConfig, config)); diff --git a/generators/setup-env/utils.js b/generators/setup-env/utils.js new file mode 100644 index 0000000..83068ae --- /dev/null +++ b/generators/setup-env/utils.js @@ -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 +}; diff --git a/package.json b/package.json index de02a4b..132b8f9 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/test/generators/setup-env/assets/moduleIndex.js b/test/generators/setup-env/assets/moduleIndex.js new file mode 100644 index 0000000..e5851e7 --- /dev/null +++ b/test/generators/setup-env/assets/moduleIndex.js @@ -0,0 +1,7 @@ +'use strict'; + +const foo = require('path'); + +module.exports = { + foo +}; diff --git a/test/generators/setup-env/setupEnvTest.js b/test/generators/setup-env/setupEnvTest.js new file mode 100644 index 0000000..17b6811 --- /dev/null +++ b/test/generators/setup-env/setupEnvTest.js @@ -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'); + }); + }); + +});