const path = require('path') // Connectors const logs = require('../connectors/logs') const sharedData = require('../connectors/shared-data') const views = require('../connectors/views') const suggestions = require('../connectors/suggestions') const folders = require('../connectors/folders') const progress = require('../connectors/progress') // Utils const ipc = require('../util/ipc') const { notify } = require('../util/notification') const { matchesPluginId } = require('@vue/cli-shared-utils') const { log } = require('../util/logger') // Validators const { validateConfiguration } = require('./configuration') const { validateDescribeTask, validateAddTask } = require('./task') const { validateClientAddon } = require('./client-addon') const { validateView, validateBadge } = require('./view') const { validateNotify } = require('./notify') const { validateSuggestion } = require('./suggestion') const { validateProgress } = require('./progress') class PluginApi { constructor ({ plugins, file, project, lightMode = false }, context) { // Context this.context = context this.pluginId = null this.project = project this.plugins = plugins this.cwd = file this.lightMode = lightMode // Hooks this.hooks = { projectOpen: [], pluginReload: [], configRead: [], configWrite: [], taskRun: [], taskExit: [], taskOpen: [], viewOpen: [] } // Data this.configurations = [] this.describedTasks = [] this.addedTasks = [] this.clientAddons = [] this.views = [] this.actions = new Map() this.ipcHandlers = [] } /** * Register an handler called when the project is open (only if this plugin is loaded). * * @param {function} cb Handler */ onProjectOpen (cb) { if (this.lightMode) return if (this.project) { cb(this.project) return } this.hooks.projectOpen.push(cb) } /** * Register an handler called when the plugin is reloaded. * * @param {function} cb Handler */ onPluginReload (cb) { if (this.lightMode) return this.hooks.pluginReload.push(cb) } /** * Register an handler called when a config is red. * * @param {function} cb Handler */ onConfigRead (cb) { if (this.lightMode) return this.hooks.configRead.push(cb) } /** * Register an handler called when a config is written. * * @param {function} cb Handler */ onConfigWrite (cb) { if (this.lightMode) return this.hooks.configWrite.push(cb) } /** * Register an handler called when a task is run. * * @param {function} cb Handler */ onTaskRun (cb) { if (this.lightMode) return this.hooks.taskRun.push(cb) } /** * Register an handler called when a task has exited. * * @param {function} cb Handler */ onTaskExit (cb) { if (this.lightMode) return this.hooks.taskExit.push(cb) } /** * Register an handler called when the user opens one task details. * * @param {function} cb Handler */ onTaskOpen (cb) { if (this.lightMode) return this.hooks.taskOpen.push(cb) } /** * Register an handler called when a view is open. * * @param {function} cb Handler */ onViewOpen (cb) { if (this.lightMode) return this.hooks.viewOpen.push(cb) } /** * Describe a project configuration (usually for config file like `.eslintrc.json`). * * @param {object} options Configuration description */ describeConfig (options) { if (this.lightMode) return try { validateConfiguration(options) this.configurations.push({ ...options, pluginId: this.pluginId }) } catch (e) { logs.add({ type: 'error', tag: 'PluginApi', message: `(${this.pluginId || 'unknown plugin'}) 'describeConfig' options are invalid\n${e.message}` }, this.context) console.error(new Error(`Invalid options: ${e.message}`)) } } /** * Describe a project task with additional information. * The tasks are generated from the scripts in the project `package.json`. * * @param {object} options Task description */ describeTask (options) { try { validateDescribeTask(options) this.describedTasks.push({ ...options, pluginId: this.pluginId }) } catch (e) { logs.add({ type: 'error', tag: 'PluginApi', message: `(${this.pluginId || 'unknown plugin'}) 'describeTask' options are invalid\n${e.message}` }, this.context) console.error(new Error(`Invalid options: ${e.message}`)) } } /** * Get the task description matching a script command. * * @param {string} command Npm script command from `package.json` * @returns {object} Task description */ getDescribedTask (command) { return this.describedTasks.find( options => options.match.test(command) ) } /** * Add a new task indepently from the scripts. * The task will only appear in the UI. * * @param {object} options Task description */ addTask (options) { try { validateAddTask(options) this.addedTasks.push({ ...options, pluginId: this.pluginId }) } catch (e) { logs.add({ type: 'error', tag: 'PluginApi', message: `(${this.pluginId || 'unknown plugin'}) 'addTask' options are invalid\n${e.message}` }, this.context) console.error(new Error(`Invalid options: ${e.message}`)) } } /** * Register a client addon (a JS bundle which will be loaded in the browser). * Used to load components and add vue-router routes. * * @param {object} options Client addon options * { * id: string, * url: string * } * or * { * id: string, * path: string * } */ addClientAddon (options) { if (this.lightMode) return try { validateClientAddon(options) if (options.url && options.path) { throw new Error(`'url' and 'path' can't be defined at the same time.`) } this.clientAddons.push({ ...options, pluginId: this.pluginId }) } catch (e) { logs.add({ type: 'error', tag: 'PluginApi', message: `(${this.pluginId || 'unknown plugin'}) 'addClientAddon' options are invalid\n${e.message}` }, this.context) console.error(new Error(`Invalid options: ${e.message}`)) } } /* Project view */ /** * Add a new project view below the builtin 'plugins', 'config' and 'tasks' ones. * * @param {object} options ProjectView options */ addView (options) { if (this.lightMode) return try { validateView(options) this.views.push({ ...options, pluginId: this.pluginId }) } catch (e) { logs.add({ type: 'error', tag: 'PluginApi', message: `(${this.pluginId || 'unknown plugin'}) 'addView' options are invalid\n${e.message}` }, this.context) console.error(new Error(`Invalid options: ${e.message}`)) } } /** * Add a badge to the project view button. * If the badge already exists, add 1 to the counter. * * @param {string} viewId Project view id * @param {object} options Badge options */ addViewBadge (viewId, options) { if (this.lightMode) return try { validateBadge(options) views.addBadge({ viewId, badge: options }, this.context) } catch (e) { logs.add({ type: 'error', tag: 'PluginApi', message: `(${this.pluginId || 'unknown plugin'}) 'addViewBadge' options are invalid\n${e.message}` }, this.context) console.error(new Error(`Invalid options: ${e.message}`)) } } /** * Remove 1 from the counter of a badge if it exists. * If the badge counter reaches 0, it is removed from the button. * * @param {any} viewId * @param {any} badgeId * @memberof PluginApi */ removeViewBadge (viewId, badgeId) { views.removeBadge({ viewId, badgeId }, this.context) } /* IPC */ /** * Add a listener to the IPC messages. * * @param {function} cb Callback with 'data' param */ ipcOn (cb) { this.ipcHandlers.push(cb) return ipc.on(cb) } /** * Remove a listener for IPC messages. * * @param {any} cb Callback to be removed */ ipcOff (cb) { const index = this.ipcHandlers.indexOf(cb) if (index !== -1) this.ipcHandlers.splice(index, 1) ipc.off(cb) } /** * Send an IPC message to all connected IPC clients. * * @param {any} data Message data */ ipcSend (data) { ipc.send(data) } /** * Get the local DB instance (lowdb) * * @readonly */ get db () { return this.context.db } /** * Display a notification in the user OS * @param {object} options Notification options */ notify (options) { try { validateNotify(options) notify(options) } catch (e) { logs.add({ type: 'error', tag: 'PluginApi', message: `(${this.pluginId || 'unknown plugin'}) 'notify' options are invalid\n${e.message}` }, this.context) console.error(new Error(`Invalid options: ${e.message}`)) } } /** * Indicates if a specific plugin is used by the project * @param {string} id Plugin id or short id */ hasPlugin (id) { if (id === 'router') id = 'vue-router' if (['vue-router', 'vuex'].includes(id)) { const pkg = folders.readPackage(this.cwd, this.context, true) return ((pkg.dependencies && pkg.dependencies[id]) || (pkg.devDependencies && pkg.devDependencies[id])) } return this.plugins.some(p => matchesPluginId(id, p.id)) } /** * Display the progress screen. * * @param {object} options Progress options */ setProgress (options) { if (this.lightMode) return try { validateProgress(options) progress.set({ ...options, id: '__plugins__' }, this.context) } catch (e) { logs.add({ type: 'error', tag: 'PluginApi', message: `(${this.pluginId || 'unknown plugin'}) 'setProgress' options are invalid\n${e.message}` }, this.context) console.error(new Error(`Invalid options: ${e.message}`)) } } /** * Remove the progress screen. */ removeProgress () { progress.remove('__plugins__', this.context) } /** * Get current working directory. */ getCwd () { return this.cwd } /** * Resolves a file relative to current working directory * @param {string} file Path to file relative to project */ resolve (file) { return path.resolve(this.cwd, file) } /** * Get currently open project */ getProject () { return this.project } /* Namespaced */ /** * Retrieve a Shared data value. * * @param {string} id Id of the Shared data * @returns {any} Shared data value */ getSharedData (id) { return sharedData.get({ id, projectId: this.project.id }, this.context) } /** * Set or update the value of a Shared data * * @param {string} id Id of the Shared data * @param {any} value Value of the Shared data */ setSharedData (id, value) { sharedData.set({ id, projectId: this.project.id, value }, this.context) } /** * Delete a shared data. * * @param {string} id Id of the Shared data */ removeSharedData (id) { sharedData.remove({ id, projectId: this.project.id }, this.context) } /** * Watch for a value change of a shared data * * @param {string} id Id of the Shared data * @param {function} handler Callback */ watchSharedData (id, handler) { sharedData.watch({ id, projectId: this.project.id }, handler) } /** * Delete the watcher of a shared data. * * @param {string} id Id of the Shared data * @param {function} handler Callback */ unwatchSharedData (id, handler) { sharedData.unwatch({ id, projectId: this.project.id }, handler) } /** * Listener triggered when a Plugin action is called from a client addon component. * * @param {string} id Id of the action to listen * @param {any} cb Callback (ex: (params) => {} ) */ onAction (id, cb) { let list = this.actions.get(id) if (!list) { list = [] this.actions.set(id, list) } list.push(cb) } /** * Call a Plugin action. This can also listened by client addon components. * * @param {string} id Id of the action * @param {object} params Params object passed as 1st argument to callbacks * @returns {Promise} */ callAction (id, params) { const plugins = require('../connectors/plugins') return plugins.callAction({ id, params }, this.context) } /** * Retrieve a value from the local DB * * @param {string} id Path to the item * @returns Item value */ storageGet (id) { return this.db.get(id).value() } /** * Store a value into the local DB * * @param {string} id Path to the item * @param {any} value Value to be stored (must be serializable in JSON) */ storageSet (id, value) { log('Storage set', id, value) this.db.set(id, value).write() } /** * Add a suggestion for the user. * * @param {object} options Suggestion */ addSuggestion (options) { if (this.lightMode) return try { validateSuggestion(options) suggestions.add(options, this.context) } catch (e) { logs.add({ type: 'error', tag: 'PluginApi', message: `(${this.pluginId || 'unknown plugin'}) 'addSuggestion' options are invalid\n${e.message}` }, this.context) console.error(new Error(`Invalid options: ${e.message}`)) } } /** * Remove a suggestion * * @param {string} id Id of the suggestion */ removeSuggestion (id) { suggestions.remove(id, this.context) } /** * Create a namespaced version of: * - getSharedData * - setSharedData * - onAction * - callAction * * @param {string} namespace Prefix to add to the id params * @returns {object} Namespaced methods */ namespace (namespace) { return { getSharedData: (id) => this.getSharedData(namespace + id), setSharedData: (id, value) => this.setSharedData(namespace + id, value), removeSharedData: (id) => this.removeSharedData(namespace + id), watchSharedData: (id, handler) => this.watchSharedData(namespace + id, handler), unwatchSharedData: (id, handler) => this.unwatchSharedData(namespace + id, handler), onAction: (id, cb) => this.onAction(namespace + id, cb), callAction: (id, params) => this.callAction(namespace + id, params), storageGet: (id) => this.storageGet(namespace + id), storageSet: (id, value) => this.storageSet(namespace + id, value), addSuggestion: (options) => { options.id = namespace + options.id return this.addSuggestion(options) }, removeSuggestion: (id) => this.removeSuggestion(namespace + id) } } } module.exports = PluginApi