1 | import ask from 'ask-nicely';
|
2 | import { readFileSync } from 'fs';
|
3 | import os from 'os';
|
4 | import path from 'path';
|
5 | import { fileURLToPath } from 'url';
|
6 |
|
7 | import addCoreModulesToApp from './addCoreModulesToApp.js';
|
8 | import addModulesToApp from './addModulesToApp.js';
|
9 | import AssertableWritableStream from './AssertableWritableStream.js';
|
10 | import ConfigManager from './ConfigManager.js';
|
11 | import createConfigFileInHomedir from './createConfigFileInHomedir.js';
|
12 | import enrichRequestObject from './enrichRequestObject.js';
|
13 | import FdtCommand from './FdtCommand.js';
|
14 | import getParentDirectoryContainingFileSync from './getParentDirectoryContainingFileSync.js';
|
15 | import ModuleRegistrationApi from './ModuleRegistrationApi.js';
|
16 | import FdtResponse from './response/FdtResponse.js';
|
17 |
|
18 | const __filename = fileURLToPath(import.meta.url);
|
19 | const __dirname = path.dirname(__filename);
|
20 |
|
21 | let packageJson = {};
|
22 | try {
|
23 | packageJson = JSON.parse(
|
24 | readFileSync(path.resolve(__dirname, '..', 'package.json'), 'utf8')
|
25 | );
|
26 | } catch (_err) {
|
27 |
|
28 |
|
29 | }
|
30 |
|
31 | const DEFAULT_OPTIONS = {
|
32 | appName: 'fdt',
|
33 | appVersion: packageJson.version,
|
34 | catchErrors: true,
|
35 | commandClass: FdtCommand,
|
36 | configFilename: '.fdtrc',
|
37 | configHomedirPath: os.homedir(),
|
38 | configPath: path.join(__dirname, '..'),
|
39 | hideStacktraceOnErrors: !process.env.FDT_STACK_TRACE_ON_ERROR,
|
40 | };
|
41 |
|
42 | function enableTestMode(options) {
|
43 | options.catchErrors = false;
|
44 | options.useTestOutput = true;
|
45 |
|
46 | if (!options.configFilename) {
|
47 | options.configFilename = '.fdttestrc';
|
48 | }
|
49 |
|
50 |
|
51 | if (options.skipCreateConfigFileInHomedir === undefined) {
|
52 | options.skipCreateConfigFileInHomedir = true;
|
53 | }
|
54 | }
|
55 |
|
56 | function enableTestOutput(app, options) {
|
57 | if (options.stdout === undefined && options.silent === undefined) {
|
58 | app.testOutput = new AssertableWritableStream({ stripAnsi: true });
|
59 | options.stdout = app.testOutput;
|
60 | }
|
61 | }
|
62 |
|
63 | export default class FontoXMLDevelopmentToolsApp {
|
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 | async init(options = {}) {
|
105 |
|
106 |
|
107 | if (options.testMode) {
|
108 | enableTestMode(options);
|
109 | }
|
110 |
|
111 |
|
112 | if (options.useTestOutput) {
|
113 | enableTestOutput(this, options);
|
114 | }
|
115 |
|
116 |
|
117 | options = { ...DEFAULT_OPTIONS, ...options };
|
118 |
|
119 | this.name = options.appName;
|
120 | this.version = options.appVersion;
|
121 |
|
122 | this.catchErrors = options.catchErrors;
|
123 | this.hideStacktraceOnErrors = !!options.hideStacktraceOnErrors;
|
124 | this.processPath = process.cwd();
|
125 |
|
126 | let configLocation = getParentDirectoryContainingFileSync(
|
127 | this.processPath,
|
128 | options.configFilename
|
129 | );
|
130 |
|
131 | if (!configLocation) {
|
132 |
|
133 |
|
134 | if (!options.skipCreateConfigFileInHomedir) {
|
135 | createConfigFileInHomedir(options);
|
136 | }
|
137 |
|
138 | configLocation = options.configHomedirPath;
|
139 | }
|
140 |
|
141 | this.config = new ConfigManager(
|
142 | configLocation,
|
143 | options.configPath,
|
144 | options.configFilename
|
145 | );
|
146 |
|
147 |
|
148 | const colorConfig = null;
|
149 |
|
150 | this.logger = new FdtResponse(colorConfig, {
|
151 | indentation: ' ',
|
152 | stdout: options.silent
|
153 | ? { write: () => {} }
|
154 | : options.stdout || process.stdout,
|
155 | });
|
156 |
|
157 | const CommandClass = options.commandClass;
|
158 | if (
|
159 | CommandClass !== FdtCommand &&
|
160 | !(CommandClass.prototype instanceof FdtCommand)
|
161 | ) {
|
162 | throw new Error(
|
163 | 'Optional option commandClass does not inherit from FdtCommand.'
|
164 | );
|
165 | }
|
166 | this.cli = new CommandClass(this.name);
|
167 | Object.assign(this.cli, ask);
|
168 |
|
169 | this.modules = [];
|
170 | this.builtInModules = [];
|
171 | await addCoreModulesToApp(this, options);
|
172 |
|
173 | this.request = Object.create(null);
|
174 | if (!options.skipEnrichRequestObject) {
|
175 | await enrichRequestObject(this);
|
176 | }
|
177 |
|
178 | if (!options.skipAddModules) {
|
179 | await addModulesToApp(this);
|
180 | }
|
181 | return this;
|
182 | }
|
183 |
|
184 | |
185 |
|
186 |
|
187 |
|
188 |
|
189 |
|
190 |
|
191 | getPathToModule(moduleName) {
|
192 | for (const module of this.modules) {
|
193 | const moduleInfo = module.getInfo();
|
194 | if (moduleInfo.name === moduleName) {
|
195 | return moduleInfo.path;
|
196 | }
|
197 | }
|
198 | return undefined;
|
199 | }
|
200 |
|
201 | |
202 |
|
203 |
|
204 |
|
205 |
|
206 |
|
207 |
|
208 |
|
209 |
|
210 | async enableBuiltInModule(modulePath, ...extra) {
|
211 | const enabledModule = await this.enableModule(modulePath, ...extra);
|
212 |
|
213 | if (enabledModule) {
|
214 | this.builtInModules.push(enabledModule);
|
215 | }
|
216 |
|
217 | return enabledModule;
|
218 | }
|
219 |
|
220 | |
221 |
|
222 |
|
223 |
|
224 |
|
225 |
|
226 |
|
227 |
|
228 | async silentEnableModule(modulePath, ...extra) {
|
229 | const enabledModule = new ModuleRegistrationApi(this, modulePath);
|
230 | const modInfo = enabledModule.getInfo();
|
231 |
|
232 | const modulesWithSameName = this.modules.filter(
|
233 | (mod) => mod.getInfo().name === modInfo.name
|
234 | );
|
235 | if (modulesWithSameName.length) {
|
236 | return null;
|
237 | }
|
238 |
|
239 | await enabledModule.load(...extra);
|
240 |
|
241 | this.modules.push(enabledModule);
|
242 |
|
243 | return enabledModule;
|
244 | }
|
245 |
|
246 | |
247 |
|
248 |
|
249 |
|
250 |
|
251 |
|
252 |
|
253 |
|
254 | async enableModule(modulePath, ...extra) {
|
255 | let enabledModule = await this.silentEnableModule(modulePath, ...extra);
|
256 |
|
257 | if (!enabledModule) {
|
258 | enabledModule = new ModuleRegistrationApi(this, modulePath);
|
259 | const modInfo = enabledModule.getInfo();
|
260 | const moduleWithSameName = this.modules
|
261 | .filter((mod) => mod.getInfo().name === modInfo.name)
|
262 | .map((mod) => {
|
263 | const info = mod.getInfo();
|
264 | return info.path + (info.builtIn ? ' (built-in)' : '');
|
265 | });
|
266 | this.logger.break();
|
267 | this.logger.notice(
|
268 | `Not loading module with name "${modInfo.path}", a module with the same name is already loaded.`
|
269 | );
|
270 | this.logger.list(moduleWithSameName, '-');
|
271 | this.logger.debug(
|
272 | `You can check your modules with \`${
|
273 | this.getInfo().name
|
274 | } module --list --verbose\` and remove the conflicting module(s) with \`${
|
275 | this.getInfo().name
|
276 | } module --remove <modulePath>\`.`
|
277 | );
|
278 | return null;
|
279 | }
|
280 |
|
281 | return enabledModule;
|
282 | }
|
283 |
|
284 | |
285 |
|
286 |
|
287 |
|
288 |
|
289 | getInfo() {
|
290 | return {
|
291 | name: this.name,
|
292 | version: this.version,
|
293 | };
|
294 | }
|
295 |
|
296 | |
297 |
|
298 |
|
299 |
|
300 |
|
301 |
|
302 |
|
303 |
|
304 | run(args, request) {
|
305 | const executedRequest = this.cli.execute(
|
306 | Object.assign([], args),
|
307 | { ...(request || this.request) },
|
308 | this.logger
|
309 | );
|
310 |
|
311 | if (!this.catchErrors) {
|
312 | return executedRequest;
|
313 | }
|
314 |
|
315 | return executedRequest
|
316 | .catch((error) => {
|
317 | this.error(undefined, error, {
|
318 | cwd: this.processPath,
|
319 | node: `Version ${process.version} running on ${process.platform} ${process.arch}.`,
|
320 | fdt: `Version ${this.version}.`,
|
321 | args: [this.name]
|
322 | .concat(
|
323 | args.map((arg) =>
|
324 | arg.indexOf(' ') >= 0 ? `"${arg}"` : arg
|
325 | )
|
326 | )
|
327 | .join(' '),
|
328 | mods: this.modules
|
329 | .map(
|
330 | (mod) =>
|
331 | `${mod.getInfo().name} (${
|
332 | mod.getInfo().version
|
333 | })`
|
334 | )
|
335 | .join(os.EOL),
|
336 | });
|
337 |
|
338 |
|
339 |
|
340 | process.on('exit', function () {
|
341 | process.exit(1);
|
342 | });
|
343 | })
|
344 | .then(() => {
|
345 |
|
346 | if (os.platform() !== 'win32') {
|
347 | this.logger.break();
|
348 | }
|
349 | return this;
|
350 | });
|
351 | }
|
352 |
|
353 | |
354 |
|
355 |
|
356 |
|
357 |
|
358 | error(caption, error, debugVariables) {
|
359 | this.logger.destroyAllSpinners();
|
360 |
|
361 | if (
|
362 | error instanceof this.logger.ErrorWithInnerError ||
|
363 | error instanceof this.logger.ErrorWithSolution
|
364 | ) {
|
365 | this.logger.caption('Error');
|
366 | this.logger.error(error.message);
|
367 | if (error.solution) {
|
368 | this.logger.break();
|
369 | this.logger.notice(error.solution);
|
370 | }
|
371 | if (!this.hideStacktraceOnErrors) {
|
372 | if (error.innerError) {
|
373 | this.logger.indent();
|
374 | this.logger.caption('Inner error');
|
375 | this.logger.debug(
|
376 | error.innerError.stack || error.innerError
|
377 | );
|
378 | this.logger.outdent();
|
379 | }
|
380 | if (debugVariables) {
|
381 | this.logger.properties(debugVariables);
|
382 | }
|
383 | }
|
384 | return;
|
385 | }
|
386 | if (
|
387 | error instanceof this.cli.InputError ||
|
388 | error instanceof this.logger.InputError
|
389 | ) {
|
390 | this.logger.caption('Input error');
|
391 | this.logger.error(error.message);
|
392 | this.logger.break();
|
393 | this.logger.notice(
|
394 | 'You might be able to fix this, use the "--help" flag for usage info.'
|
395 | );
|
396 | if (error.solution) {
|
397 | this.logger.log(error.solution);
|
398 | }
|
399 | if (!this.hideStacktraceOnErrors && debugVariables) {
|
400 | this.logger.properties(debugVariables);
|
401 | }
|
402 | return;
|
403 | }
|
404 |
|
405 | this.logger.caption(caption || 'Error');
|
406 |
|
407 | if (error) {
|
408 | this.logger.error(error.message || error.stack || error);
|
409 | }
|
410 |
|
411 | if (error.solution) {
|
412 | this.logger.break();
|
413 | this.logger.notice(error.solution);
|
414 | }
|
415 |
|
416 | if (!this.hideStacktraceOnErrors) {
|
417 | if (error && error.stack) {
|
418 | this.logger.indent();
|
419 | this.logger.debug(error.stack);
|
420 | this.logger.outdent();
|
421 | }
|
422 |
|
423 | if (debugVariables) {
|
424 | this.logger.properties(debugVariables);
|
425 | }
|
426 | }
|
427 | }
|
428 | }
|