diff --git a/src/node-plop.js b/src/node-plop.js index d5858a4..2ef6ab6 100644 --- a/src/node-plop.js +++ b/src/node-plop.js @@ -9,29 +9,42 @@ import bakedInHelpers from './baked-in-helpers'; import generatorRunner from './generator-runner'; function nodePlop(plopfilePath = '', plopCfg = {}) { - let pkgJson = {}; - let defaultInclude = {generators: true}; + let defaultInclude = { generators: true }; let welcomeMessage; - const {destBasePath, force} = plopCfg; + const { destBasePath, force } = plopCfg; const generators = {}; + const generatorMixins = {}; const partials = {}; const actionTypes = {}; - const helpers = Object.assign({ - pkg: (propertyPath) => _get(pkgJson, propertyPath, '') - }, bakedInHelpers); + const helpers = Object.assign( + { + pkg: propertyPath => _get(pkgJson, propertyPath, '') + }, + bakedInHelpers + ); const baseHelpers = Object.keys(helpers); const setPrompt = inquirer.registerPrompt; - const setWelcomeMessage = (message) => { welcomeMessage = message; }; - const setHelper = (name, fn) => { helpers[name] = fn; }; - const setPartial = (name, str) => { partials[name] = str; }; - const setActionType = (name, fn) => { actionTypes[name] = fn; }; + const setWelcomeMessage = message => { + welcomeMessage = message; + }; + const setHelper = (name, fn) => { + helpers[name] = fn; + }; + const setPartial = (name, str) => { + partials[name] = str; + }; + const setActionType = (name, fn) => { + actionTypes[name] = fn; + }; function renderString(template, data) { Object.keys(helpers).forEach(h => handlebars.registerHelper(h, helpers[h])); - Object.keys(partials).forEach(p => handlebars.registerPartial(p, partials[p])); + Object.keys(partials).forEach(p => + handlebars.registerPartial(p, partials[p]) + ); return handlebars.compile(template)(data); } @@ -53,17 +66,39 @@ function nodePlop(plopfilePath = '', plopCfg = {}) { return generators[name]; } - const getHelperList = () => Object.keys(helpers).filter(h => !baseHelpers.includes(h)); + const getGeneratorMixin = name => generatorMixins[name]; + function setGeneratorMixin(name = '', config = {}) { + // if no name is provided, use a default + name = name || `generatorMixin-${Object.keys(generatorMixins).length + 1}`; + + // add the generator to this context + generatorMixins[name] = Object.assign(config, { + name: name, + basePath: plopfilePath + }); + + return generatorMixins[name]; + } + + const getHelperList = () => + Object.keys(helpers).filter(h => !baseHelpers.includes(h)); const getPartialList = () => Object.keys(partials); const getActionTypeList = () => Object.keys(actionTypes); function getGeneratorList() { - return Object.keys(generators).map(function (name) { - const {description} = generators[name]; - return {name, description}; + return Object.keys(generators).map(function(name) { + const { description } = generators[name]; + return { name, description }; }); } - const setDefaultInclude = inc => defaultInclude = inc; + function getGeneratorMixinList() { + return Object.keys(generatorMixins).map(function(name) { + const { description } = generatorMixins[name]; + return { name, description }; + }); + } + + const setDefaultInclude = inc => (defaultInclude = inc); const getDefaultInclude = () => defaultInclude; const getDestBasePath = () => destBasePath || plopfilePath; const getPlopfilePath = () => plopfilePath; @@ -77,39 +112,75 @@ function nodePlop(plopfilePath = '', plopCfg = {}) { }; function load(targets, loadCfg = {}, includeOverride) { - if (typeof targets === 'string') { targets = [targets]; } - const config = Object.assign({ - destBasePath: getDestBasePath() - }, loadCfg); + if (typeof targets === 'string') { + targets = [targets]; + } + const config = Object.assign( + { + destBasePath: getDestBasePath() + }, + loadCfg + ); - targets.forEach(function (target) { - const targetPath = resolve.sync(target, {basedir: getPlopfilePath()}); + targets.forEach(function(target) { + const targetPath = resolve.sync(target, { basedir: getPlopfilePath() }); const proxy = nodePlop(targetPath, config); const proxyDefaultInclude = proxy.getDefaultInclude() || {}; const includeCfg = includeOverride || proxyDefaultInclude; - const include = Object.assign({ - generators: false, - helpers: false, - partials: false, - actionTypes: false - }, includeCfg); + const include = Object.assign( + { + generators: false, + generatorMixins: false, + helpers: false, + partials: false, + actionTypes: false + }, + includeCfg + ); const genNameList = proxy.getGeneratorList().map(g => g.name); - loadAsset(genNameList, include.generators, setGenerator, proxyName => ({proxyName, proxy})); - loadAsset(proxy.getPartialList(), include.partials, setPartial, proxy.getPartial); - loadAsset(proxy.getHelperList(), include.helpers, setHelper, proxy.getHelper); - loadAsset(proxy.getActionTypeList(), include.actionTypes, setActionType, proxy.getActionType); + const genMixinNameList = proxy.getGeneratorList().map(g => g.name); + loadAsset(genNameList, include.generators, setGenerator, proxyName => ({ + proxyName, + proxy + })); + loadAsset( + genMixinNameList, + include.generatorMixins, + setGeneratorMixin, + proxyName => ({ proxyName, proxy }) + ); + loadAsset( + proxy.getPartialList(), + include.partials, + setPartial, + proxy.getPartial + ); + loadAsset( + proxy.getHelperList(), + include.helpers, + setHelper, + proxy.getHelper + ); + loadAsset( + proxy.getActionTypeList(), + include.actionTypes, + setActionType, + proxy.getActionType + ); }); } function loadAsset(nameList, include, addFunc, getFunc) { var incArr; - if (include === true) { incArr = nameList; } + if (include === true) { + incArr = nameList; + } if (include instanceof Array) { incArr = include.filter(n => typeof n === 'string'); } if (incArr != null) { - include = incArr.reduce(function (inc, name) { + include = incArr.reduce(function(inc, name) { inc[name] = name; return inc; }, {}); @@ -122,8 +193,11 @@ function nodePlop(plopfilePath = '', plopCfg = {}) { function loadPackageJson() { // look for a package.json file to use for the "pkg" helper - try { pkgJson = require(path.join(getDestBasePath(), 'package.json')); } - catch(error) { pkgJson = {}; } + try { + pkgJson = require(path.join(getDestBasePath(), 'package.json')); + } catch (error) { + pkgJson = {}; + } } ///////// @@ -134,24 +208,40 @@ function nodePlop(plopfilePath = '', plopCfg = {}) { const plopfileApi = { // main methods for setting and getting plop context things setPrompt, - setWelcomeMessage, getWelcomeMessage, - setGenerator, getGenerator, getGeneratorList, - setPartial, getPartial, getPartialList, - setHelper, getHelper, getHelperList, - setActionType, getActionType, getActionTypeList, + setWelcomeMessage, + getWelcomeMessage, + setGenerator, + getGenerator, + getGeneratorList, + setGeneratorMixin, + getGeneratorMixin, + getGeneratorMixinList, + setPartial, + getPartial, + getPartialList, + setHelper, + getHelper, + getHelperList, + setActionType, + getActionType, + getActionTypeList, // path context methods - setPlopfilePath, getPlopfilePath, + setPlopfilePath, + getPlopfilePath, getDestBasePath, // plop.load functionality - load, setDefaultInclude, getDefaultInclude, + load, + setDefaultInclude, + getDefaultInclude, // render a handlebars template renderString, // passthrough properties - inquirer, handlebars, + inquirer, + handlebars, // passthroughs for backward compatibility addPrompt: setPrompt, @@ -160,10 +250,49 @@ function nodePlop(plopfilePath = '', plopCfg = {}) { }; // the runner for this instance of the nodePlop api - const runner = generatorRunner(plopfileApi, {force}); + + const runner = generatorRunner(plopfileApi, { force }); + + /** + if actions is a funciton, it gets invoked with ...args and returned + otherwise actions is assumed to be an array and returned directly, + so in every case it will be an array + **/ + const normalizeActions = (actions, ...args) => { + if (actions && typeof actions === 'function') { + return actions(...args) || []; + } + return actions || []; + }; + // thx https://medium.com/@dtipson/creating-an-es6ish-compose-in-javascript-ac580b95104a + const compose = (...fns) => fns.reduce((f, g) => (...args) => f(g(...args))); + const applyGeneratorMixins = generator => { + const mixins = generator.mixins + .map(name => getGeneratorMixin(name)) + .reverse(); + const mixinActions = compose( + ...mixins.filter(m => m.actions).map(m => m.actions) + ); + const mixinPrompts = compose( + ...mixins.filter(m => m.prompts).map(m => m.prompts) + ); + + return Object.assign({}, generator, { + actions: (...actionsArgs) => + mixinActions( + normalizeActions(generator.actions, ...actionsArgs), + ...actionsArgs + ), + prompts: mixinPrompts(generator.prompts) + }); + }; + const nodePlopApi = Object.assign({}, plopfileApi, { getGenerator(name) { - var generator = plopfileApi.getGenerator(name); + let generator = plopfileApi.getGenerator(name); + if (generator.mixins) { + generator = applyGeneratorMixins(generator); + } // if this generator was loaded from an external plopfile, proxy the // generator request through to the external plop instance @@ -172,8 +301,9 @@ function nodePlop(plopfilePath = '', plopCfg = {}) { } return Object.assign({}, generator, { - runActions: (data) => runner.runGeneratorActions(generator, data), - runPrompts: (bypassArr = []) => runner.runGeneratorPrompts(generator, bypassArr) + runActions: data => runner.runGeneratorActions(generator, data), + runPrompts: (bypassArr = []) => + runner.runGeneratorPrompts(generator, bypassArr) }); }, setGenerator(name, config) { diff --git a/tests/generator-mixins-mock/plop-templates/moduleFile.txt b/tests/generator-mixins-mock/plop-templates/moduleFile.txt new file mode 100644 index 0000000..a737a39 --- /dev/null +++ b/tests/generator-mixins-mock/plop-templates/moduleFile.txt @@ -0,0 +1 @@ +this is a file named {{name}} inside module {{moduleName}} diff --git a/tests/generator-mixins-mock/plop-templates/moduleIndex.txt b/tests/generator-mixins-mock/plop-templates/moduleIndex.txt new file mode 100644 index 0000000..1678682 --- /dev/null +++ b/tests/generator-mixins-mock/plop-templates/moduleIndex.txt @@ -0,0 +1 @@ +this is the module index file for module {{moduleName}} diff --git a/tests/generator-mixins-mock/plopfile.js b/tests/generator-mixins-mock/plopfile.js new file mode 100644 index 0000000..dd026c1 --- /dev/null +++ b/tests/generator-mixins-mock/plopfile.js @@ -0,0 +1,111 @@ +module.exports = function(plop) { + 'use strict'; + + plop.setGeneratorMixin('withModule', { + description: 'adds module to the generator', + prompts: prompts => + [ + { + type: 'input', + name: 'moduleName', + message: 'What is the modulename?', + validate: function(value) { + if (/.+/.test(value)) { + return true; + } + return 'moduleName is required'; + } + } + ].concat(prompts), + actions: baseActions => { + const modulePath = 'src/{{moduleName}}'; + return [ + { + type: 'add', + path: modulePath + '/index.txt', + templateFile: 'plop-templates/moduleIndex.txt', + abortOnFail: true + } + ].concat( + // also extend `path` on every base action + (baseActions || []).map(config => + Object.assign({}, config, { path: modulePath + '/' + config.path }) + ) + ); + } + }); + + plop.setGeneratorMixin('withLog', { + description: 'logs actions', + + actions: baseActions => { + const template = + 'created these files:\n\n' + baseActions.map(a => a.path).join('\n'); + const writeLog = { + type: 'add', + path: 'src/log.txt', + template, + abortOnFail: true + }; + + return baseActions.concat(writeLog); + } + }); + + plop.setGenerator('module', { + description: 'adds only the module', + mixins: ['withModule'] + }); + + plop.setGenerator('module-file', { + description: 'adds a file inside a module', + mixins: ['withModule'], + prompts: [ + { + type: 'input', + name: 'name', + message: 'What is the file name?', + validate: function(value) { + if (/.+/.test(value)) { + return true; + } + return 'name is required'; + } + } + ], + actions: [ + { + type: 'add', + path: 'files/{{name}}.txt', + templateFile: 'plop-templates/moduleFile.txt', + abortOnFail: true + } + ] + }); + + plop.setGenerator('module-file-with-log', { + description: 'adds a file inside a module', + mixins: ['withModule', 'withLog'], + prompts: [ + { + type: 'input', + name: 'name', + message: 'What is the file name?', + validate: function(value) { + if (/.+/.test(value)) { + return true; + } + return 'name is required'; + } + } + ], + actions: [ + { + type: 'add', + path: 'files/{{name}}.txt', + templateFile: 'plop-templates/moduleFile.txt', + abortOnFail: true + } + ] + }); +}; diff --git a/tests/generator-mixins.ava.js b/tests/generator-mixins.ava.js new file mode 100644 index 0000000..5904129 --- /dev/null +++ b/tests/generator-mixins.ava.js @@ -0,0 +1,59 @@ +import fs from 'fs'; +import path from 'path'; +import co from 'co'; +import AvaTest from './_base-ava-test'; +const { test, mockPath, testSrcPath, nodePlop } = new AvaTest(__filename); + +const plop = nodePlop(`${mockPath}/plopfile.js`); +const moduleGenerator = plop.getGenerator('module'); +const moduleFileGenerator = plop.getGenerator('module-file'); +const moduleFileWithLogGenerator = plop.getGenerator('module-file-with-log'); + +test( + 'Check that the module index file has been created if module generator is run standalone', + co.wrap(function*(t) { + yield moduleGenerator.runActions({ moduleName: 'sampleModule' }); + const indexPath = path.resolve(testSrcPath, 'sampleModule/index.txt'); + t.true(fs.existsSync(indexPath)); + }) +); + +test( + 'Check that both index file and the module file are generaded', + co.wrap(function*(t) { + yield moduleFileGenerator.runActions({ + moduleName: 'myFirstModule', + name: 'myFile' + }); + const indexPath = path.resolve(testSrcPath, 'myFirstModule/index.txt'); + t.true(fs.existsSync(indexPath)); + const moduleFilePath = path.resolve( + testSrcPath, + 'myFirstModule/files/myFile.txt' + ); + t.true(fs.existsSync(moduleFilePath)); + }) +); + +test( + 'Check that the log mixin writes an additional file containing both files created', + co.wrap(function*(t) { + yield moduleFileWithLogGenerator.runActions({ + moduleName: 'myFirstModule', + name: 'myFile' + }); + + const logFile = path.resolve(testSrcPath, 'log.txt'); + + t.true(fs.existsSync(logFile)); + const content = fs.readFileSync(logFile).toString(); + + t.is( + content, + `created these files: + +src/myFirstModule/index.txt +src/myFirstModule/files/myFile.txt` + ); + }) +);