UNPKG

8.52 kBJavaScriptView Raw
1import ask from 'ask-nicely';
2import path from 'path';
3import { fileURLToPath, pathToFileURL } from 'url';
4
5const __filename = fileURLToPath(import.meta.url);
6const __dirname = path.dirname(__filename);
7
8const FILENAMES_TO_SKIP_FOR_SET_CONTROLLER_CALLSITES = Symbol(
9 'FILENAMES_TO_SKIP_FOR_SET_CONTROLLER_CALLSITES'
10);
11
12/**
13 * A FDT custom version of AskNicely#Command that has extra options for checking Fonto specific commands.
14 *
15 * @augments AskNicely.Command
16 */
17export default class FdtCommand extends ask.Command {
18 /**
19 * @constructor
20 * @param {string} commandName
21 * @param {function(AskNicelyRequest, FdtResponse)} [controller]
22 */
23 constructor(commandName, controller) {
24 super(commandName, controller);
25
26 this._moduleRegistration = null;
27
28 this.examples = [];
29 this.isHelpCommand = false;
30 this.longDescription = null;
31
32 this.hideIfMissingRequiredProductLicenses = false;
33 this.requiredProductLicenses = [];
34 this.requiresEditorRepository = false;
35 this.requiresToValidateLicenseFile = false;
36
37 this.rawOutput = false;
38
39 this.setNewChildClass(FdtCommand);
40
41 this.addPreController(this._preController.bind(this));
42 }
43
44 static addFileNameToSkipForSetControllerCallsites(fileNameToSkip) {
45 FdtCommand[FILENAMES_TO_SKIP_FOR_SET_CONTROLLER_CALLSITES].push(
46 path.join(fileNameToSkip)
47 );
48 }
49
50 /**
51 * Get the module registration to which this command, or it's ancestors, belongs.
52 *
53 * @return {ModuleRegistration|null} The module registration.
54 */
55 getModuleRegistration() {
56 let command = this;
57 while (command) {
58 if (command._moduleRegistration) {
59 return command._moduleRegistration;
60 }
61 command = command.parent;
62 }
63 return null;
64 }
65
66 _createLazyLoadController(controllerSource) {
67 if (!path.isAbsolute(controllerSource)) {
68 throw new Error(
69 `Command controller "${controllerSource}" should be registered using an absolute path instead of a relative one.`
70 );
71 }
72
73 return (...args) => {
74 return import(pathToFileURL(controllerSource)).then((module) =>
75 module.default(...args)
76 );
77 };
78 }
79
80 /**
81 * Set the main controller.
82 *
83 * @param {string|function(AskNicelyRequest, FdtResponse)} [controller]
84 *
85 * @return {FdtCommand} This command.
86 */
87 setController(controller) {
88 if (typeof controller === 'string') {
89 controller = this._createLazyLoadController(controller);
90 }
91
92 return super.setController(controller);
93 }
94
95 /**
96 * Register an example usage of this command. Each example is rendered as a definition item,
97 * meaning the definition is indented below the caption.
98 *
99 * @param {string} caption
100 * @param {string} content
101 *
102 * @return {FdtCommand}
103 */
104 addExample(caption, content) {
105 this.examples.push({
106 caption,
107 content,
108 });
109
110 return this;
111 }
112
113 /**
114 * Register a hidden command as a child of this, and register this as parent of the child.
115 *
116 * @param {string|Command} commandName The identifying name of this option, unique for its ancestry.
117 * @param {string|function(AskNicelyRequest, FdtResponse)} [controller]
118 *
119 * @return {Command} The child command.
120 */
121 addHiddenCommand(commandName, controller) {
122 const addedCommand = super.addCommand(commandName, controller);
123 addedCommand.hidden = true;
124 return addedCommand;
125 }
126
127 /**
128 * Describe a hidden option.
129 *
130 * @param {Option|string} long The identifying name of this option, unique for its ancestry.
131 * @param {string} [short] A one-character alias of this option, unique for its ancestry.
132 * @param {string} [description] A description for the option.
133 * @param {boolean} [required] If true, an omittance would throw an error.
134 *
135 * @return {Command}
136 */
137 addHiddenOption(long, short, description, required) {
138 super.addOption(long, short, description, required);
139 const addedOption = this.options[this.options.length - 1];
140 addedOption.hidden = true;
141 return this;
142 }
143
144 /**
145 * Register a long description for a command - one that is printed in the full width of the terminal.
146 *
147 * @param {string} description
148 * @return {FdtCommand}
149 */
150 setLongDescription(description) {
151 this.longDescription = description;
152
153 return this;
154 }
155
156 /**
157 * Configure this command to not execute a controller, but instead output the help as if the
158 * --help option was passed to the command.
159 *
160 * @param {boolean} enabled
161 *
162 * @return {FdtCommand}
163 */
164 setAsHelpCommand(enabled) {
165 this.isHelpCommand = enabled !== undefined ? !!enabled : true;
166
167 return this;
168 }
169
170 /**
171 * Gets the "long" name of a command, which includes the parameters and the long names of it's ancestors.
172 *
173 * @return {string}
174 */
175 getLongName() {
176 return (this.parent ? [this.parent.getLongName()] : [])
177 .concat([this.name])
178 .concat(this.parameters.map((parameter) => `<${parameter.name}>`))
179 .join(' ');
180 }
181
182 /**
183 * Pre-controller which performs optional checks before executing the actual controller.
184 *
185 * @param {Object} req
186 * @param {Object} res
187 *
188 * @return {Promise}
189 */
190 _preController(req, res) {
191 let preControllerPromise = Promise.resolve();
192
193 if (this.requiresEditorRepository) {
194 preControllerPromise = preControllerPromise.then(() => {
195 req.fdt.editorRepository.throwIfNotInsideEditorRepository();
196 });
197 }
198
199 if (this.requiresToValidateLicenseFile) {
200 preControllerPromise = preControllerPromise.then(() => {
201 const destroySpinner = res.spinner('Validating license...');
202
203 return req.fdt.license
204 .validateAndUpdateLicenseFile()
205 .then((result) => {
206 destroySpinner();
207 return result;
208 })
209 .catch((error) => {
210 destroySpinner();
211 throw error;
212 });
213 });
214 }
215
216 if (this.requiredProductLicenses.length > 0) {
217 preControllerPromise = preControllerPromise.then(() => {
218 const destroySpinner = res.spinner(
219 'Checking required product licenses...'
220 );
221
222 try {
223 req.fdt.license.ensureProductLicenses(
224 this.requiredProductLicenses
225 );
226 destroySpinner();
227 } catch (error) {
228 destroySpinner();
229 throw error;
230 }
231
232 return true;
233 });
234 }
235
236 return preControllerPromise;
237 }
238
239 /**
240 * Add a list of required product licenses for performing this command. If the user does not have
241 * access to a product, the command will fail.
242 *
243 * @param {Array<string>} productIds
244 *
245 * @return {FdtCommand}
246 */
247 addRequiredProductLicenses(productIds) {
248 this.requiredProductLicenses =
249 this.requiredProductLicenses.concat(productIds);
250
251 return this;
252 }
253
254 /**
255 * Hide the command if a required product license is not available.
256 *
257 * @param {boolean} [hide=true]
258 *
259 * @return {FdtCommand}
260 */
261 setHideIfMissingRequiredProductLicenses(hide = true) {
262 this.hideIfMissingRequiredProductLicenses = !!hide;
263
264 return this;
265 }
266
267 /**
268 * If set, check the license for validity online before executing the command, fail
269 * otherwise.
270 *
271 * @param {boolean} [validateLicense=true]
272 *
273 * @return {FdtCommand}
274 */
275 setRequiresLicenseValidation(validateLicense = true) {
276 this.requiresToValidateLicenseFile = !!validateLicense;
277
278 return this;
279 }
280
281 /**
282 * If set, check if running from an editor repository before executing the command, fail
283 * otherwise.
284 *
285 * @param {boolean} [value=true]
286 *
287 * @return {FdtCommand}
288 */
289 setRequiresEditorRepository(value = true) {
290 this.requiresEditorRepository = !!value;
291
292 return this;
293 }
294
295 /**
296 * If set, the output will be optimized for raw/machine readable output.
297 *
298 * Accepts either a boolean, or a function, which receives the request object, and should return
299 * a boolean indicating if the command should use raw output. This can be useful when raw output
300 * is dependend on, for example, an option.
301 *
302 * Setting to, or returning, true has the following impact:
303 * * Use response.raw() to skip any output formatting.
304 *
305 * @param {boolean|function(request): boolean} [value=true]
306 *
307 * @return {FdtCommand}
308 */
309 setRawOutput(value = true) {
310 this.rawOutput = value;
311
312 return this;
313 }
314}
315
316const fdtBasename = path.basename(path.resolve(__dirname, '..'));
317FdtCommand[FILENAMES_TO_SKIP_FOR_SET_CONTROLLER_CALLSITES] = [];
318FdtCommand.addFileNameToSkipForSetControllerCallsites(
319 '/ask-nicely/dist/AskNicely.js'
320);
321FdtCommand.addFileNameToSkipForSetControllerCallsites(
322 `/${fdtBasename}/src/FdtCommand.js`
323);
324FdtCommand.addFileNameToSkipForSetControllerCallsites(
325 `/${fdtBasename}/src/ModuleRegistrationApi.js`
326);
327FdtCommand.addFileNameToSkipForSetControllerCallsites(
328 `/${fdtBasename}/src/response/FdtResponse.js`
329);