Skip to content

Commit 6d374d8

Browse files
authored
Merge pull request plopjs#35 from amwmedia/nicoespeon-add-many-action
Nicoespeon add many action
2 parents b06e957 + b5d009c commit 6d374d8

37 files changed

+589
-254
lines changed

README.md

+2-2
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ const plop = nodePlop(`./path/to/plopfile.js`);
1515
const basicAdd = plop.getGenerator('basic-add');
1616

1717
// run all the generator actions using the data specified
18-
basicAdd.runActions({name: 'this is a test'}).then(function () {
19-
// do something after the actions have run successfully
18+
basicAdd.runActions({name: 'this is a test'}).then(function (results) {
19+
// do something after the actions have run
2020
});
2121
```

package.json

+6-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "node-plop",
3-
"version": "0.5.5",
3+
"version": "0.6.0",
44
"description": "programmatic plopping for fun and profit",
55
"main": "lib/index.js",
66
"scripts": {
@@ -24,7 +24,10 @@
2424
},
2525
"keywords": [
2626
"plop",
27+
"generator",
28+
"scaffolding",
2729
"node",
30+
"programmatic",
2831
"automation"
2932
],
3033
"author": "Andrew Worcester <[email protected]> (http://amwmedia.com)",
@@ -45,6 +48,7 @@
4548
"eslint-config-standard": "^6.2.1",
4649
"eslint-plugin-promise": "^3.3.1",
4750
"eslint-plugin-standard": "^2.0.1",
51+
"plop": "^1.7.4",
4852
"plop-pack-fancy-comments": "^0.2.1",
4953
"pre-commit": "^1.1.3"
5054
},
@@ -54,6 +58,7 @@
5458
"colors": "^1.1.2",
5559
"core-js": "^2.4.1",
5660
"del": "^2.2.1",
61+
"globby": "^6.1.0",
5762
"handlebars": "^4.0.5",
5863
"inquirer": "^1.2.0",
5964
"lodash.get": "^4.4.2",
+38
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import path from 'path';
2+
import * as fspp from '../fs-promise-proxy';
3+
4+
export default function* addFile(data, cfg, plop) {
5+
// if not already an absolute path, make an absolute path from the basePath (plopfile location)
6+
const makeTmplPath = p => path.resolve(plop.getPlopfilePath(), p);
7+
const makeDestPath = p => path.resolve(plop.getDestBasePath(), p);
8+
9+
var {template} = cfg;
10+
const fileDestPath = makeDestPath(plop.renderString(cfg.path || '', data));
11+
12+
try {
13+
if (cfg.templateFile) {
14+
template = yield fspp.readFile(makeTmplPath(cfg.templateFile));
15+
}
16+
if (template == null) { template = ''; }
17+
18+
// check path
19+
const pathExists = yield fspp.fileExists(fileDestPath);
20+
21+
if (pathExists) {
22+
throw `File already exists\n -> ${fileDestPath}`;
23+
} else {
24+
yield fspp.makeDir(path.dirname(fileDestPath));
25+
yield fspp.writeFile(fileDestPath, plop.renderString(template, data));
26+
}
27+
28+
// return the added file path (relative to the destination path)
29+
return fileDestPath.replace(path.resolve(plop.getDestBasePath()), '');
30+
31+
} catch(err) {
32+
if (typeof err === 'string') {
33+
throw err;
34+
} else {
35+
throw err.message || JSON.stringify(err);
36+
}
37+
}
38+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
export default function(action) {
2+
3+
// it's not even an object, you fail!
4+
if (typeof action !== 'object') {
5+
return `Invalid action object: ${JSON.stringify(action)}`;
6+
}
7+
8+
const {path} = action;
9+
10+
if (typeof path !== 'string' || path.length === 0) {
11+
return `Invalid path "${path}"`;
12+
}
13+
14+
return true;
15+
}

src/actions/add.js

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import co from 'co';
2+
import actionInterfaceTest from './_common-action-interface-check';
3+
import addFile from './_common-action-add-file';
4+
5+
export default co.wrap(function* (data, cfg, plop) {
6+
const interfaceTestResult = actionInterfaceTest(cfg);
7+
if (interfaceTestResult !== true) { throw interfaceTestResult; }
8+
9+
return yield addFile(data, cfg, plop);
10+
});

src/actions/addMany.js

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import co from 'co';
2+
import path from 'path';
3+
import globby from 'globby';
4+
import actionInterfaceTest from './_common-action-interface-check';
5+
import addFile from './_common-action-add-file';
6+
7+
export default co.wrap(function* (data, cfg, plop) {
8+
const cfgWithCommonInterface = Object.assign({}, cfg, {
9+
path: cfg.destination
10+
});
11+
const interfaceTestResult = actionInterfaceTest(cfgWithCommonInterface);
12+
if (interfaceTestResult !== true) { throw interfaceTestResult; }
13+
14+
const templateFiles = resolveTemplateFiles(cfg.templateFiles, cfg.base, plop);
15+
const filesAdded = [];
16+
for (let templateFile of templateFiles) {
17+
const fileCfg = Object.assign({}, cfg, {
18+
path: resolvePath(cfg.destination, templateFile, cfg.base),
19+
templateFile: templateFile
20+
});
21+
22+
const addedPath = yield addFile(data, fileCfg, plop);
23+
filesAdded.push(addedPath);
24+
}
25+
26+
return `${filesAdded.length} files added\n -> ${filesAdded.join('\n -> ')}`;
27+
});
28+
29+
function resolveTemplateFiles(templateFilesGlob, basePath, plop) {
30+
return globby.sync([templateFilesGlob], { cwd: plop.getPlopfilePath() })
31+
.filter(isUnder(basePath))
32+
.filter(isFile);
33+
}
34+
35+
function isFile(file) {
36+
const fileParts = file.split(path.sep);
37+
const lastFilePart = fileParts[fileParts.length - 1];
38+
const hasExtension = !!(lastFilePart.split('.')[1]);
39+
40+
return hasExtension;
41+
}
42+
43+
function isUnder(basePath = '') {
44+
return (path) => path.startsWith(basePath);
45+
}
46+
47+
function resolvePath(destination, file, rootPath) {
48+
return path.join(destination, dropFileRootPath(file, rootPath));
49+
}
50+
51+
function dropFileRootPath(file, rootPath) {
52+
return (rootPath) ? file.replace(rootPath, '') : dropFileRootFolder(file);
53+
}
54+
55+
function dropFileRootFolder(file) {
56+
const fileParts = file.split(path.sep);
57+
fileParts.shift();
58+
59+
return fileParts.join(path.sep);
60+
}

src/actions/modify.js

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import co from 'co';
2+
import path from 'path';
3+
import * as fspp from '../fs-promise-proxy';
4+
import actionInterfaceTest from './_common-action-interface-check';
5+
6+
export default co.wrap(function* (data, cfg, plop) {
7+
const interfaceTestResult = actionInterfaceTest(cfg);
8+
if (interfaceTestResult !== true) { throw interfaceTestResult; }
9+
10+
// if not already an absolute path, make an absolute path from the basePath (plopfile location)
11+
const makeTmplPath = p => path.resolve(plop.getPlopfilePath(), p);
12+
const makeDestPath = p => path.resolve(plop.getDestBasePath(), p);
13+
14+
var {template} = cfg;
15+
const fileDestPath = makeDestPath(plop.renderString(cfg.path || '', data));
16+
17+
try {
18+
if (cfg.templateFile) {
19+
template = yield fspp.readFile(makeTmplPath(cfg.templateFile));
20+
}
21+
if (template == null) { template = ''; }
22+
23+
// check path
24+
const pathExists = yield fspp.fileExists(fileDestPath);
25+
26+
if (!pathExists) {
27+
throw 'File does not exists';
28+
} else {
29+
var fileData = yield fspp.readFile(fileDestPath);
30+
fileData = fileData.replace(cfg.pattern, plop.renderString(template, data));
31+
yield fspp.writeFile(fileDestPath, fileData);
32+
}
33+
34+
// return the modified file path (relative to the destination path)
35+
return fileDestPath.replace(path.resolve(plop.getDestBasePath()), '');
36+
} catch(err) {
37+
if (typeof err === 'string') {
38+
throw err;
39+
} else {
40+
throw err.message || JSON.stringify(err);
41+
}
42+
}
43+
});
File renamed without changes.

src/modules/fs-promise-proxy.js renamed to src/fs-promise-proxy.js

+1
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const _writeFile = pify(fs.writeFile);
77
const _access = pify(fs.access);
88

99
export const makeDir = pify(mkdirp);
10+
export const readdir = pify(fs.readdir);
1011
export const readFile = path => _readFile(path, 'utf8');
1112
export const writeFile = (path, data) => _writeFile(path, data, 'utf8');
1213
export const fileExists = path => _access(path).then(() => true, () => false);

src/generator-runner.js

+121
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
'use strict';
2+
3+
import co from 'co';
4+
import colors from 'colors';
5+
import add from './actions/add';
6+
import addMany from './actions/addMany';
7+
import modify from './actions/modify';
8+
9+
export default function (plopfileApi) {
10+
var abort;
11+
12+
// triggers inquirer with the correct prompts for this generator
13+
// returns a promise that resolves with the user's answers
14+
const runGeneratorPrompts = co.wrap(function* (genObject) {
15+
if (genObject.prompts == null) {
16+
throw Error(`${genObject.name} has no prompts`);
17+
}
18+
return yield plopfileApi.inquirer.prompt(genObject.prompts);
19+
});
20+
21+
// Run the actions for this generator
22+
const runGeneratorActions = co.wrap(function* (genObject, data) {
23+
var changes = []; // array of changed made by the actions
24+
var failures = []; // array of actions that failed
25+
var {actions} = genObject; // the list of actions to execute
26+
const customActionTypes = getCustomActionTypes();
27+
const buildInActions = { add, addMany, modify };
28+
const actionTypes = Object.assign({}, customActionTypes, buildInActions);
29+
30+
abort = false;
31+
32+
// if action is a function, run it to get our array of actions
33+
if (typeof actions === 'function') { actions = actions(data); }
34+
35+
// if actions are not defined... we cannot proceed.
36+
if (actions == null) {
37+
throw Error(`${genObject.name} has no actions`);
38+
}
39+
40+
// if actions are not an array, invalid!
41+
if (!(actions instanceof Array)) {
42+
throw Error(`${genObject.name} has invalid actions`);
43+
}
44+
45+
for (let [actionIdx, action] of actions.entries()) {
46+
// bail out if a previous action aborted
47+
if (abort) {
48+
failures.push({
49+
type: action.type || '',
50+
path: action.path || '',
51+
error: 'Aborted due to previous action failure'
52+
});
53+
continue;
54+
}
55+
56+
const actionIsFunction = typeof action === 'function';
57+
const actionCfg = (actionIsFunction ? {} : action);
58+
const actionLogic = (actionIsFunction ? action : actionTypes[actionCfg.type]);
59+
60+
if (typeof actionLogic !== 'function') {
61+
if (actionCfg.abortOnFail !== false) { abort = true; }
62+
failures.push({
63+
type: action.type || '',
64+
path: action.path || '',
65+
error: `Invalid action (#${actionIdx + 1})`
66+
});
67+
continue;
68+
}
69+
70+
try {
71+
const actionResult = yield executeActionLogic(actionLogic, actionCfg, data);
72+
changes.push(actionResult);
73+
} catch(failure) {
74+
failures.push(failure);
75+
}
76+
}
77+
78+
return { changes, failures };
79+
});
80+
81+
// handle action logic
82+
const executeActionLogic = co.wrap(function* (action, cfg, data) {
83+
const failure = makeErrorLogger(cfg.type || 'function', '', cfg.abortOnFail);
84+
85+
// convert any returned data into a promise to
86+
// return and wait on
87+
return yield Promise.resolve(action(data, cfg, plopfileApi)).then(
88+
// show the resolved value in the console
89+
result => ({
90+
type: cfg.type || 'function',
91+
path: colors.blue(result.toString())
92+
}),
93+
// a rejected promise is treated as a failure
94+
function (err) {
95+
throw failure(err.message || err.toString());
96+
}
97+
);
98+
});
99+
100+
// request the list of custom actions from the plopfile
101+
function getCustomActionTypes() {
102+
return plopfileApi.getActionTypeList()
103+
.reduce(function (types, name) {
104+
types[name] = plopfileApi.getActionType(name);
105+
return types;
106+
}, {});
107+
}
108+
109+
// provide a function to handle action errors in a uniform way
110+
function makeErrorLogger(type, path, abortOnFail) {
111+
return function (error) {
112+
if (abortOnFail !== false) { abort = true; }
113+
return { type, path, error };
114+
};
115+
}
116+
117+
return {
118+
runGeneratorActions,
119+
runGeneratorPrompts
120+
};
121+
}

src/index.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import 'core-js'; // es2015 polyfill
2-
import nodePlop from './modules/node-plop';
2+
import nodePlop from './node-plop';
33

44
/**
55
* Main node-plop module
66
*
77
* @param {string} plopfilePath - The absolute path to the plopfile we are interested in working with
8+
* @param {object} plopCfg - A config object to be passed into the plopfile when it's executed
89
* @returns {object} the node-plop API for the plopfile requested
910
*/
1011
module.exports = nodePlop;

0 commit comments

Comments
 (0)