­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ ­ // @ts-check const fs = require('fs-extra'); const os = require('os'); const path = require('path'); const mapValues = require('lodash/mapValues'); /** * @typedef {import('./instance')} Instance * @typedef {import('./ui')} UI * @typedef {import('./system')} System */ /** * Handles various extension-related behavior * * @class Extension */ class Extension { /** * Gets the available process managers (if any) from this extension * By default it checks the package.json for any listed ones, but subclasses * can override this method to do it a different way. * * @property processManagers * @type Object * @public */ get processManagers() { if (!this.pkg || !this.pkg['ghost-cli'] || !this.pkg['ghost-cli']['process-managers']) { return {}; } return mapValues(this.pkg['ghost-cli']['process-managers'], relPath => path.join(this.dir, relPath)); } /** * Creates the extension instance * * @param {UI} ui UI instance * @param {System} system System instance * @param {Object} pkg Package.json contents * @param {string} dir Extension directory */ constructor(ui, system, pkg, dir) { this.ui = ui; this.system = system; this.pkg = pkg; this.dir = dir; } /** * Creates a system config file (used mainly by extensions to configure external services) * Creates a local copy of the config file in /system/files, then links * the file into the directory needed by the service * * @param {Instance} instance Ghost instance reference * @param {string} contents Contents of the template file to create * @param {string} descriptor Description of the template file (used in the prompt) * @param {string} file Filename * @param {string} dir Directory to move the config file into * @return {Promise} * @method template * @public */ async template(instance, contents, descriptor, file, dir) { if (this.ui.verbose && (await this.ui.confirm(`Would you like to view the ${descriptor} file?`, false))) { this.ui.log(contents); } const tmpDir = path.join(os.tmpdir(), instance.name); await fs.ensureDir(tmpDir); const tmpFile = path.join(tmpDir, file); await fs.writeFile(tmpFile, contents || ''); const outputLocation = path.join(dir, file); await this.ui.sudo(`mv ${tmpFile} ${outputLocation}`); } /** * Gets the Extension instance for a given extension. An extension can * extend this class and override some of it's features. If no such subclass exists * an instance of the base class (this one) will be returned * * @param {UI} ui UI instance * @param {System} system System instance * @param {Object} extension Object (returned from find-plugins) containing the * package.json contents and the dir of the extension * @return Extension * @static * @method getInstance * @private */ static getInstance(ui, system, extension) { const pkg = extension.pkg; const dir = extension.dir; // TODO: currently this returns an instance of the base class only if // the pkg.main doesn't exist. If any errors are generated by these checks here, // nothing is returned. We should probably either throw an error here or return // an instance of the base class if (pkg.main) { if (!fs.existsSync(path.join(dir, pkg.main))) { ui.log(`Extension '${pkg.name}' has a main entry, but no such file exists`, 'yellow'); return; } const ExtensionSubclass = require(path.join(dir, path.basename(pkg.main, '.js'))); if (!ExtensionSubclass || !(ExtensionSubclass.prototype instanceof Extension)) { ui.log(`Extension '${pkg.name}' supplied a main file, but it is not a valid Extension subclass`, 'yellow'); return; } return new ExtensionSubclass(ui, system, pkg, dir); } return new this(ui, system, pkg, dir); } } module.exports = Extension;