UNPKG

apostrophe

Version:
378 lines (344 loc) • 13 kB
// This module allows other modules to create command line tasks. // // A command line task is invoked like this: // // node app @apostrophecms/migration:migrate // // Apostrophe is fully initialized before your task is run, except that it does // not listen for connections. So you may access all of its features in your // task. // Direct use of `console` makes sense here because we're implementing an // interaction at the CLI. const _ = require('lodash'); const { stripIndent } = require('common-tags'); const { SemanticAttributes } = require('@opentelemetry/semantic-conventions'); module.exports = { options: { alias: 'task' }, handlers(self) { return { 'apostrophe:run': { async runTask(isTask, after) { if (!isTask) { return; } const cmd = self.apos.argv._[0]; const telemetry = self.apos.telemetry; const spanName = `task:${cmd}`; await telemetry.startActiveSpan(spanName, async (span) => { span.setAttribute(SemanticAttributes.CODE_FUNCTION, 'runTask'); span.setAttribute(SemanticAttributes.CODE_NAMESPACE, '@apostrophecms/task'); span.setAttribute( telemetry.Attributes.ARGV, telemetry.stringify(self.apos.argv) ); let task; if (!cmd) { const err = 'There is no command line argument to serve as a task name, should never happen'; console.error(err); return self.exit(after, 1, span, err); } if (cmd === 'help') { span.setAttribute(telemetry.Attributes.TARGET_FUNCTION, 'help'); // list all tasks if (self.apos.argv._.length === 1) { return self.usage(after, span); } // help with specific task if (self.apos.argv._.length === 2) { span.setAttribute( telemetry.Attributes.TARGET_NAMESPACE, self.apos.argv._[1] ); task = self.find(self.apos.argv._[1]); if (!task) { console.error('There is no such task.'); return self.usage(after, span, 'There is no such task.'); } if (task.usage) { console.log(`\nTips for the ${task.fullName} task:\n`); console.log(task.usage); } else { console.log('That is a valid task, but it does not have a help message.'); } return self.exit(after, 0, span); } } task = self.find(cmd); if (!task) { console.error('\nThere is no such task.'); return self.usage(after, span, `There is no such task ${cmd}`); } const [ moduleName, taskName ] = task.fullName.split(':'); span.setAttribute(telemetry.Attributes.TARGET_NAMESPACE, moduleName); span.setAttribute(telemetry.Attributes.TARGET_FUNCTION, taskName); try { await task.task(self.apos.argv); return self.exit(after, 0, span); } catch (e) { console.error(e.message); return self.exit(after, 1, span, e); } }); } } }; }, methods(self) { return { // For use when you wish to execute an Apostrophe command line task from // your code and continue, without using the command line or using the // `child_process` module. // // Except for `name`, all arguments may be omitted. // // This is an async function and should be awaited. // // Examples (assume `products` extends `@apostrophecms/piece-type`): // // `await self.apos.task.invoke('@apostrophecms/user:add', [ 'admin', // 'admin' ])` // // `await self.apos.task.invoke('products:generate', { total: 20 })` // // The `args` and `options` arguments may be completely omitted. // // If present, `args` contains an array of positional arguments to // the task, **not including** the task name. // // If present, `options` contains the optional parameters that would // normally be hyphenated, i.e. at the command line you might write // `--total=20`. // // This method will return the value returned by the task. // // **Gotchas** // // If you can invoke a method directly rather than invoking a task, do // that. This method is for cases where that option is not readily // available. // // During the execution of the task, `self.apos.argv` will have a new, // temporary value to accommodate tasks that inspect this property // directly rather than examining their `argv` argument. `self.apos.argv` // will be restored at the end of task execution. // // Some tasks may not be written to be "good neighbors." For instance, a // task developer might assume they can exit the process directly. async invoke(name, args, options) { let result; const telemetry = self.apos.telemetry; const spanName = `task:${self.__meta.name}:${name}`; const aposArgv = self.apos.argv; await telemetry.startActiveSpan(spanName, async (span) => { span.setAttribute(SemanticAttributes.CODE_FUNCTION, 'invoke'); span.setAttribute(SemanticAttributes.CODE_NAMESPACE, '@apostrophecms/task'); try { if (Array.isArray(args)) { args.splice(0, 0, name); } else { options = args; args = [ name ]; } const task = self.find(name); const [ moduleName, taskName ] = task.fullName.split(':'); span.setAttribute(telemetry.Attributes.TARGET_NAMESPACE, moduleName); span.setAttribute(telemetry.Attributes.TARGET_FUNCTION, taskName); const argv = { _: args, ...options || {} }; span.setAttribute(telemetry.Attributes.ARGV, telemetry.stringify(argv)); self.apos.argv = argv; result = await task.task(argv); span.setStatus({ code: telemetry.api.SpanStatusCode.OK }); } catch (err) { telemetry.handleError(span, err); throw err; } finally { self.apos.argv = aposArgv; span.end(); } }); return result; }, // Identifies the task corresponding to the given command line argument. find(fullName) { const matches = fullName.match(/^(.*?):(.*)$/); if (!matches) { return false; } const moduleName = matches[1]; const name = matches[2]; if ( !(self.apos.modules[moduleName] && _.has(self.apos.modules[moduleName].tasks, name)) ) { return false; } const task = self.apos.modules[moduleName].tasks[name]; task.fullName = `${moduleName}:${name}`; return task; }, // Displays a usage message, including a list of available tasks, // and exits the entire program with a nonzero status code. // Forward signal, span and error to the exit handler. usage(after, span, err) { // Direct use of console makes sense in tasks. -Tom console.error('\nThe following tasks are available:\n'); for (const [ moduleName, module ] of Object.entries(self.apos.modules)) { for (const name of Object.keys(module.tasks)) { console.error(`${moduleName}:${name}`); } } console.error('\nType:\n'); console.error('node app help groupname:taskname\n'); console.error('To get help with a specific task.\n'); console.error('To launch the site, run with no arguments.'); return self.exit(after, 1, span, err); }, // Register error (if any) and close the current telemetry span; // send a signal back to the bootstrap to exit the process with a given // code. exit(after, code, span, err) { after.exit = code; if (!span) { return; } if (err) { self.apos.telemetry.handleError(span, err); } else if (code) { span.setStatus({ code: self.apos.telemetry.api.SpanStatusCode.ERROR }); } else { span.setStatus({ code: self.apos.telemetry.api.SpanStatusCode.OK }); } span.end(); }, // Return a `req` object suitable for command line tasks // and unit tests. The `req` object returned is a mockup of a true Express // `req` object. // // An `options` object may be passed. If `options.role` is set, // it may be `anon` (no role and no req.user), `guest`, `contributor`, // `editor`, or `admin`. For bc reasons, it defaults to `admin`. // // Other properties of `options` are assigned as properties of the // returned `req` object before any initialization tasks such as computing // `req.absoluteUrl`. This facilitates unit testing. getReq(options) { options = options || {}; options.role = options.role || 'admin'; const req = { ...(options.role === 'anon' ? {} : { user: { title: 'System Task', role: options.role } }), res: { redirect(url) { req.res.redirectedTo = url; }, header(key, value) { req.res.headers = { ...(req.res.headers || {}), [key]: value }; } }, t(key, options = {}) { return self.apos.i18n.i18next.t(key, { ...options, lng: req.locale }); }, body: {}, data: {}, headers: {}, protocol: 'http', get: function (propName) { return { Host: 'you-need-to-set-baseUrl-in-app-js.com' }[propName]; }, query: {}, url: '/', locale: self.apos.argv.locale || self.apos.modules['@apostrophecms/i18n'].defaultLocale, mode: 'published', aposNeverLoad: {}, aposStack: [], __(key) { self.apos.util.warnDevOnce('old-i18n-req-helper', stripIndent` The req.__() and res.__() functions are deprecated and do not localize in A3. Use req.t instead. `); return key; }, session: {} }; addCloneMethod(req); req.res.__ = req.__; const { role: _role, staticBuild: _staticBuild, ...properties } = options || {}; // When staticBuild is requested, configure the req object // with the same properties that Express middlewares if (_staticBuild) { self.apos.modules['@apostrophecms/express'].applyStaticBuildHeaders(req); } Object.assign(req, properties); self.apos.i18n.setPrefixUrls(req); return req; function addCloneMethod(req) { req.clone = (properties = {}) => { const _req = { ...req, ...properties }; self.apos.i18n.setPrefixUrls(_req); addCloneMethod(_req); return _req; }; } }, // Convenience wrapper for `getReq`. Returns a request // object simulating an anonymous site visitor, with no role // and no `req.user`. getAnonReq(options) { return self.getReq({ role: 'anon', ...options }); }, // Convenience wrapper for `getReq`. Returns a request // object simulating a user with the guest role. getGuestReq(options) { return self.getReq({ role: 'guest', ...options }); }, // Convenience wrapper for `getReq`. Returns a request // object simulating a user with the contributor role. getContributorReq(options) { return self.getReq({ role: 'contributor', ...options }); }, // Convenience wrapper for `getReq`. Returns a request // object simulating a user with the editor role. getEditorReq(options) { return self.getReq({ role: 'editor', ...options }); }, // Convenience wrapper for `getReq`. Returns a request // object simulating a user with the admin role. getAdminReq(options) { // For bc reasons this is the default behavior of getReq return self.getReq(options); } }; } };