-
Notifications
You must be signed in to change notification settings - Fork 276
/
generator-runner.js
199 lines (171 loc) · 5.62 KB
/
generator-runner.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
"use strict";
import promptBypass from "./prompt-bypass.js";
import * as buildInActions from "./actions/index.js";
export default function (plopfileApi, flags) {
let abort;
// triggers inquirer with the correct prompts for this generator
// returns a promise that resolves with the user's answers
const runGeneratorPrompts = async function (genObject, bypassArr = []) {
const { prompts } = genObject;
if (prompts == null) {
throw Error(`${genObject.name} has no prompts`);
}
if (typeof prompts === "function") {
return await prompts(plopfileApi.inquirer);
}
// handle bypass data when provided
const [promptsAfterBypass, bypassAnswers] = await promptBypass(
prompts,
bypassArr,
plopfileApi,
);
return await plopfileApi.inquirer
.prompt(promptsAfterBypass)
.then((answers) => Object.assign(answers, bypassAnswers));
};
// Run the actions for this generator
const runGeneratorActions = async function (
genObject,
data = {},
hooks = {},
) {
const noop = () => {};
const {
onSuccess = noop, // runs after each successful action
onFailure = noop, // runs after each failed action
onComment = noop, // runs for each comment line in the actions array
} = hooks;
const changes = []; // array of changed made by the actions
const failures = []; // array of actions that failed
let { actions } = genObject; // the list of actions to execute
const customActionTypes = getCustomActionTypes();
const actionTypes = Object.assign({}, customActionTypes, buildInActions);
abort = false;
// if action is a function, run it to get our array of actions
if (typeof actions === "function") {
actions = actions(data);
}
// if actions are not defined... we cannot proceed.
if (actions == null) {
throw Error(`${genObject.name} has no actions`);
}
// if actions are not an array, invalid!
if (!Array.isArray(actions)) {
throw Error(`${genObject.name} has invalid actions`);
}
for (let [actionIdx, action] of actions.entries()) {
// including strings in the actions array is used for commenting
if (typeof action === "string" && abort) {
continue;
}
if (typeof action === "string") {
onComment(action);
continue;
}
const actionIsFunction = typeof action === "function";
const actionCfg = actionIsFunction ? { type: "function" } : action;
const actionLogic = actionIsFunction
? action
: actionTypes[actionCfg.type];
// bail out if a previous action aborted
if (abort) {
const failure = {
type: actionCfg.type || "",
path: actionCfg.path || "",
error: "Aborted due to previous action failure",
};
onFailure(failure);
failures.push(failure);
continue;
}
actionCfg.force = flags.force === true || actionCfg.force === true;
if (typeof actionLogic !== "function") {
if (actionCfg.abortOnFail !== false) {
abort = true;
}
const failure = {
type: actionCfg.type || "",
path: actionCfg.path || "",
error: `Invalid action (#${actionIdx + 1})`,
};
onFailure(failure);
failures.push(failure);
continue;
}
try {
const actionResult = await executeActionLogic(
actionLogic,
actionCfg,
data,
);
onSuccess(actionResult);
changes.push(actionResult);
} catch (failure) {
if (actionCfg.abortOnFail !== false) {
abort = true;
}
onFailure(failure);
failures.push(failure);
}
}
return { changes, failures };
};
// handle action logic
const executeActionLogic = async function (action, cfg, data) {
const type = cfg.type || "";
let cfgData = cfg.data || {};
// data can also be a function that returns a data object
if (typeof cfgData === "function") {
cfgData = await cfgData();
}
// check if action should run
if (typeof cfg.skip === "function") {
// Merge main data and config data in new object
const reasonToSkip = await cfg.skip({ ...data, ...cfgData });
if (typeof reasonToSkip === "string") {
// Return actionResult instead of string
return {
type: "skip",
path: reasonToSkip,
};
}
}
// track keys that can be applied to the main data scope
const cfgDataKeys = Object.keys(cfgData).filter(
(k) => typeof data[k] === "undefined",
);
// copy config data into main data scope so it's available for templates
cfgDataKeys.forEach((k) => {
data[k] = cfgData[k];
});
return await Promise.resolve(action(data, cfg, plopfileApi))
.then(
// show the resolved value in the console
(result) => ({
type,
path: typeof result === "string" ? result : JSON.stringify(result),
}),
// a rejected promise is treated as a failure
(err) => {
throw { type, path: "", error: err.message || err.toString() };
},
)
// cleanup main data scope so config data doesn't leak
.finally(() =>
cfgDataKeys.forEach((k) => {
delete data[k];
}),
);
};
// request the list of custom actions from the plopfile
function getCustomActionTypes() {
return plopfileApi.getActionTypeList().reduce(function (types, name) {
types[name] = plopfileApi.getActionType(name);
return types;
}, {});
}
return {
runGeneratorActions,
runGeneratorPrompts,
};
}