123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683 |
- const path = require('path')
- const fs = require('fs-extra')
- const LRU = require('lru-cache')
- const { chalk } = require('@vue/cli-shared-utils')
- // Context
- const getContext = require('../context')
- // Subs
- const channels = require('../channels')
- // Connectors
- const cwd = require('./cwd')
- const folders = require('./folders')
- const prompts = require('./prompts')
- const progress = require('./progress')
- const logs = require('./logs')
- const clientAddons = require('./client-addons')
- const views = require('./views')
- const locales = require('./locales')
- const sharedData = require('./shared-data')
- const suggestions = require('./suggestions')
- const dependencies = require('./dependencies')
- // Api
- const PluginApi = require('../api/PluginApi')
- // Utils
- const {
- isPlugin,
- isOfficialPlugin,
- getPluginLink,
- resolveModule,
- loadModule,
- clearModule,
- execa
- } = require('@vue/cli-shared-utils')
- const { progress: installProgress } = require('@vue/cli/lib/util/executeCommand')
- const PackageManager = require('@vue/cli/lib/util/ProjectPackageManager')
- const ipc = require('../util/ipc')
- const { log } = require('../util/logger')
- const { notify } = require('../util/notification')
- const PROGRESS_ID = 'plugin-installation'
- const CLI_SERVICE = '@vue/cli-service'
- // Caches
- const logoCache = new LRU({
- max: 50
- })
- // Local
- let currentPluginId
- let eventsInstalled = false
- let installationStep
- const pluginsStore = new Map()
- const pluginApiInstances = new Map()
- const pkgStore = new Map()
- async function list (file, context, { resetApi = true, lightApi = false, autoLoadApi = true } = {}) {
- let pkg = folders.readPackage(file, context)
- let pkgContext = cwd.get()
- // Custom package.json location
- if (pkg.vuePlugins && pkg.vuePlugins.resolveFrom) {
- pkgContext = path.resolve(cwd.get(), pkg.vuePlugins.resolveFrom)
- pkg = folders.readPackage(pkgContext, context)
- }
- pkgStore.set(file, { pkgContext, pkg })
- let plugins = []
- plugins = plugins.concat(findPlugins(pkg.devDependencies || {}, file))
- plugins = plugins.concat(findPlugins(pkg.dependencies || {}, file))
- // Put cli service at the top
- const index = plugins.findIndex(p => p.id === CLI_SERVICE)
- if (index !== -1) {
- const service = plugins[index]
- plugins.splice(index, 1)
- plugins.unshift(service)
- }
- pluginsStore.set(file, plugins)
- log('Plugins found:', plugins.length, chalk.grey(file))
- if (resetApi || (autoLoadApi && !pluginApiInstances.has(file))) {
- await resetPluginApi({ file, lightApi }, context)
- }
- return plugins
- }
- function findOne ({ id, file }, context) {
- const plugins = getPlugins(file)
- const plugin = plugins.find(
- p => p.id === id
- )
- if (!plugin) log('Plugin Not found', id, chalk.grey(file))
- return plugin
- }
- function findPlugins (deps, file) {
- return Object.keys(deps).filter(
- id => isPlugin(id) || id === CLI_SERVICE
- ).map(
- id => ({
- id,
- versionRange: deps[id],
- official: isOfficialPlugin(id) || id === CLI_SERVICE,
- installed: fs.existsSync(dependencies.getPath({ id, file })),
- website: getLink(id),
- baseDir: file
- })
- )
- }
- function getLink (id) {
- if (id === CLI_SERVICE) return 'https://cli.vuejs.org/'
- return getPluginLink(id)
- }
- function getPlugins (file) {
- const plugins = pluginsStore.get(file)
- if (!plugins) return []
- return plugins
- }
- function resetPluginApi ({ file, lightApi }, context) {
- return new Promise((resolve, reject) => {
- log('Plugin API reloading...', chalk.grey(file))
- const widgets = require('./widgets')
- let pluginApi = pluginApiInstances.get(file)
- let projectId
- // Clean up
- if (pluginApi) {
- projectId = pluginApi.project.id
- pluginApi.views.forEach(r => views.remove(r.id, context))
- pluginApi.ipcHandlers.forEach(fn => ipc.off(fn))
- }
- if (!lightApi) {
- if (projectId) sharedData.unWatchAll({ projectId }, context)
- clientAddons.clear(context)
- suggestions.clear(context)
- widgets.reset(context)
- }
- // Cyclic dependency with projects connector
- setTimeout(async () => {
- const projects = require('./projects')
- const project = projects.findByPath(file, context)
- if (!project) {
- resolve(false)
- return
- }
- const plugins = getPlugins(file)
- if (project && projects.getType(project, context) !== 'vue') {
- resolve(false)
- return
- }
- pluginApi = new PluginApi({
- plugins,
- file,
- project,
- lightMode: lightApi
- }, context)
- pluginApiInstances.set(file, pluginApi)
- // Run Plugin API
- runPluginApi(path.resolve(__dirname, '../../'), pluginApi, context, 'ui-defaults')
- plugins.forEach(plugin => runPluginApi(plugin.id, pluginApi, context))
- // Local plugins
- const { pkg, pkgContext } = pkgStore.get(file)
- if (pkg.vuePlugins && pkg.vuePlugins.ui) {
- const files = pkg.vuePlugins.ui
- if (Array.isArray(files)) {
- for (const file of files) {
- runPluginApi(pkgContext, pluginApi, context, file)
- }
- }
- }
- // Add client addons
- pluginApi.clientAddons.forEach(options => {
- clientAddons.add(options, context)
- })
- // Add views
- for (const view of pluginApi.views) {
- await views.add({ view, project }, context)
- }
- // Register widgets
- for (const definition of pluginApi.widgetDefs) {
- await widgets.registerDefinition({ definition, project }, context)
- }
- if (lightApi) {
- resolve(true)
- return
- }
- if (projectId !== project.id) {
- callHook({
- id: 'projectOpen',
- args: [project, projects.getLast(context)],
- file
- }, context)
- } else {
- callHook({
- id: 'pluginReload',
- args: [project],
- file
- }, context)
- // View open hook
- const currentView = views.getCurrent()
- if (currentView) views.open(currentView.id)
- }
- // Load widgets for current project
- widgets.load(context)
- resolve(true)
- })
- })
- }
- function runPluginApi (id, pluginApi, context, filename = 'ui') {
- const name = filename !== 'ui' ? `${id}/${filename}` : id
- let module
- try {
- module = loadModule(`${id}/${filename}`, pluginApi.cwd, true)
- } catch (e) {
- if (process.env.VUE_CLI_DEBUG) {
- console.error(e)
- }
- }
- if (module) {
- if (typeof module !== 'function') {
- log(`${chalk.red('ERROR')} while loading plugin API: no function exported, for`, name, chalk.grey(pluginApi.cwd))
- logs.add({
- type: 'error',
- message: `An error occurred while loading ${name}: no function exported`
- })
- } else {
- pluginApi.pluginId = id
- try {
- module(pluginApi)
- log('Plugin API loaded for', name, chalk.grey(pluginApi.cwd))
- } catch (e) {
- log(`${chalk.red('ERROR')} while loading plugin API for ${name}:`, e)
- logs.add({
- type: 'error',
- message: `An error occurred while loading ${name}: ${e.message}`
- })
- }
- pluginApi.pluginId = null
- }
- }
- // Locales
- try {
- const folder = fs.existsSync(id) ? id : dependencies.getPath({ id, file: pluginApi.cwd })
- locales.loadFolder(folder, context)
- } catch (e) {}
- }
- function getApi (folder) {
- const pluginApi = pluginApiInstances.get(folder)
- return pluginApi
- }
- function callHook ({ id, args, file }, context) {
- const pluginApi = getApi(file)
- if (!pluginApi) return
- const fns = pluginApi.hooks[id]
- log(`Hook ${id}`, fns.length, 'handlers')
- fns.forEach(fn => fn(...args))
- }
- async function getLogo (plugin, context) {
- const { id, baseDir } = plugin
- const cached = logoCache.get(id)
- if (cached) {
- return cached
- }
- const folder = dependencies.getPath({ id, file: baseDir })
- const file = path.join(folder, 'logo.png')
- if (fs.existsSync(file)) {
- const data = `/_plugin-logo/${encodeURIComponent(id)}`
- logoCache.set(id, data)
- return data
- }
- return null
- }
- function getInstallation (context) {
- if (!eventsInstalled) {
- eventsInstalled = true
- // Package installation progress events
- installProgress.on('progress', value => {
- if (progress.get(PROGRESS_ID)) {
- progress.set({ id: PROGRESS_ID, progress: value }, context)
- }
- })
- installProgress.on('log', message => {
- if (progress.get(PROGRESS_ID)) {
- progress.set({ id: PROGRESS_ID, info: message }, context)
- }
- })
- }
- return {
- id: 'plugin-install',
- pluginId: currentPluginId,
- step: installationStep,
- prompts: prompts.list()
- }
- }
- function install (id, context) {
- return progress.wrap(PROGRESS_ID, context, async setProgress => {
- setProgress({
- status: 'plugin-install',
- args: [id]
- })
- currentPluginId = id
- installationStep = 'install'
- if (process.env.VUE_CLI_DEBUG && isOfficialPlugin(id)) {
- mockInstall(id, context)
- } else {
- const pm = new PackageManager({ context: cwd.get() })
- await pm.add(id)
- }
- await initPrompts(id, context)
- installationStep = 'config'
- notify({
- title: 'Plugin installed',
- message: `Plugin ${id} installed, next step is configuration`,
- icon: 'done'
- })
- return getInstallation(context)
- })
- }
- function mockInstall (id, context) {
- const pkg = folders.readPackage(cwd.get(), context, true)
- pkg.devDependencies[id] = '*'
- folders.writePackage({ file: cwd.get(), data: pkg }, context)
- return true
- }
- function installLocal (context) {
- const projects = require('./projects')
- const folder = cwd.get()
- cwd.set(projects.getCurrent(context).path, context)
- return progress.wrap(PROGRESS_ID, context, async setProgress => {
- const pkg = loadModule(path.resolve(folder, 'package.json'), cwd.get(), true)
- const id = pkg.name
- setProgress({
- status: 'plugin-install',
- args: [id]
- })
- currentPluginId = id
- installationStep = 'install'
- // Update package.json
- {
- const pkgFile = path.resolve(cwd.get(), 'package.json')
- const pkg = await fs.readJson(pkgFile)
- if (!pkg.devDependencies) pkg.devDependencies = {}
- pkg.devDependencies[id] = `file:${folder}`
- await fs.writeJson(pkgFile, pkg, {
- spaces: 2
- })
- }
- const from = path.resolve(cwd.get(), folder)
- const to = path.resolve(cwd.get(), 'node_modules', ...id.split('/'))
- console.log('copying from', from, 'to', to)
- await fs.copy(from, to)
- await initPrompts(id, context)
- installationStep = 'config'
- notify({
- title: 'Plugin installed',
- message: `Plugin ${id} installed, next step is configuration`,
- icon: 'done'
- })
- return getInstallation(context)
- })
- }
- function uninstall (id, context) {
- return progress.wrap(PROGRESS_ID, context, async setProgress => {
- setProgress({
- status: 'plugin-uninstall',
- args: [id]
- })
- installationStep = 'uninstall'
- currentPluginId = id
- if (process.env.VUE_CLI_DEBUG && isOfficialPlugin(id)) {
- mockUninstall(id, context)
- } else {
- const pm = new PackageManager({ context: cwd.get() })
- await pm.remove(id)
- }
- currentPluginId = null
- installationStep = null
- notify({
- title: 'Plugin uninstalled',
- message: `Plugin ${id} uninstalled`,
- icon: 'done'
- })
- return getInstallation(context)
- })
- }
- function mockUninstall (id, context) {
- const pkg = folders.readPackage(cwd.get(), context, true)
- delete pkg.devDependencies[id]
- folders.writePackage({ file: cwd.get(), data: pkg }, context)
- return true
- }
- function runInvoke (id, context) {
- return progress.wrap(PROGRESS_ID, context, async setProgress => {
- setProgress({
- status: 'plugin-invoke',
- args: [id]
- })
- clearModule('@vue/cli-service/webpack.config.js', cwd.get())
- currentPluginId = id
- // Allow plugins that don't have a generator
- if (resolveModule(`${id}/generator`, cwd.get())) {
- const child = execa('vue', [
- 'invoke',
- id,
- '--$inlineOptions',
- JSON.stringify(prompts.getAnswers())
- ], {
- cwd: cwd.get(),
- stdio: ['inherit', 'pipe', 'inherit']
- })
- const onData = buffer => {
- const text = buffer.toString().trim()
- if (text) {
- setProgress({
- info: text
- })
- logs.add({
- type: 'info',
- message: text
- }, context)
- }
- }
- child.stdout.on('data', onData)
- await child
- }
- // Run plugin api
- runPluginApi(id, getApi(cwd.get()), context)
- installationStep = 'diff'
- notify({
- title: 'Plugin invoked successfully',
- message: `Plugin ${id} invoked successfully`,
- icon: 'done'
- })
- return getInstallation(context)
- })
- }
- function finishInstall (context) {
- installationStep = null
- currentPluginId = null
- return getInstallation(context)
- }
- async function initPrompts (id, context) {
- await prompts.reset()
- try {
- let data = require(path.join(dependencies.getPath({ id, file: cwd.get() }), 'prompts'))
- if (typeof data === 'function') {
- data = await data()
- }
- data.forEach(prompts.add)
- } catch (e) {
- console.warn(`No prompts found for ${id}`)
- }
- await prompts.start()
- }
- function update ({ id, full }, context) {
- return progress.wrap('plugin-update', context, async setProgress => {
- setProgress({
- status: 'plugin-update',
- args: [id]
- })
- currentPluginId = id
- const plugin = findOne({ id, file: cwd.get() }, context)
- const { current, wanted, localPath } = await dependencies.getVersion(plugin, context)
- if (localPath) {
- await updateLocalPackage({ cwd: cwd.get(), id, localPath, full }, context)
- } else {
- const pm = new PackageManager({ context: cwd.get() })
- await pm.upgrade(id)
- }
- logs.add({
- message: `Plugin ${id} updated from ${current} to ${wanted}`,
- type: 'info'
- }, context)
- notify({
- title: 'Plugin updated',
- message: `Plugin ${id} was successfully updated`,
- icon: 'done'
- })
- await resetPluginApi({ file: cwd.get() }, context)
- dependencies.invalidatePackage({ id }, context)
- currentPluginId = null
- return findOne({ id, file: cwd.get() }, context)
- })
- }
- async function updateLocalPackage ({ id, cwd, localPath, full = true }, context) {
- const from = path.resolve(cwd, localPath)
- const to = path.resolve(cwd, 'node_modules', ...id.split('/'))
- let filterRegEx
- if (full) {
- await fs.remove(to)
- filterRegEx = /\.git/
- } else {
- filterRegEx = /(\.git|node_modules)/
- }
- await fs.copy(from, to, {
- filter: (file) => !file.match(filterRegEx)
- })
- }
- async function updateAll (context) {
- return progress.wrap('plugins-update', context, async setProgress => {
- const plugins = await list(cwd.get(), context, { resetApi: false })
- const updatedPlugins = []
- for (const plugin of plugins) {
- const version = await dependencies.getVersion(plugin, context)
- if (version.current !== version.wanted) {
- updatedPlugins.push(plugin)
- dependencies.invalidatePackage({ id: plugin.id }, context)
- }
- }
- if (!updatedPlugins.length) {
- notify({
- title: 'No updates available',
- message: 'No plugin to update in the version ranges declared in package.json',
- icon: 'done'
- })
- return []
- }
- setProgress({
- status: 'plugins-update',
- args: [updatedPlugins.length]
- })
- const pm = new PackageManager({ context: cwd.get() })
- await pm.upgrade(updatedPlugins.map(p => p.id).join(' '))
- notify({
- title: 'Plugins updated',
- message: `${updatedPlugins.length} plugin(s) were successfully updated`,
- icon: 'done'
- })
- await resetPluginApi({ file: cwd.get() }, context)
- return updatedPlugins
- })
- }
- async function callAction ({ id, params, file = cwd.get() }, context) {
- const pluginApi = getApi(file)
- context.pubsub.publish(channels.PLUGIN_ACTION_CALLED, {
- pluginActionCalled: { id, params }
- })
- log('PluginAction called', id, params)
- const results = []
- const errors = []
- const list = pluginApi.actions.get(id)
- if (list) {
- for (const cb of list) {
- let result = null
- let error = null
- try {
- result = await cb(params)
- } catch (e) {
- error = e
- }
- results.push(result)
- errors.push(error)
- }
- }
- context.pubsub.publish(channels.PLUGIN_ACTION_RESOLVED, {
- pluginActionResolved: { id, params, results, errors }
- })
- log('PluginAction resolved', id, params, 'results:', results, 'errors:', errors)
- return { id, params, results, errors }
- }
- function serveFile ({ pluginId, projectId = null, file }, res) {
- let baseFile = cwd.get()
- if (projectId) {
- const projects = require('./projects')
- const project = projects.findOne(projectId, getContext())
- if (project) {
- baseFile = project.path
- }
- }
- if (pluginId) {
- const basePath = pluginId === '.' ? baseFile : dependencies.getPath({ id: decodeURIComponent(pluginId), file: baseFile })
- if (basePath) {
- res.sendFile(path.join(basePath, file))
- return
- }
- } else {
- console.log('serve issue', 'pluginId:', pluginId, 'projectId:', projectId, 'file:', file)
- }
- res.status(404)
- res.send('Addon not found in loaded addons. Try opening a vue-cli project first?')
- }
- function serve (req, res) {
- const { id: pluginId, 0: file } = req.params
- serveFile({ pluginId, file: path.join('ui-public', file) }, res)
- }
- function serveLogo (req, res) {
- const { id: pluginId } = req.params
- const { project: projectId } = req.query
- serveFile({ pluginId, projectId, file: 'logo.png' }, res)
- }
- module.exports = {
- list,
- findOne,
- getLogo,
- getInstallation,
- install,
- installLocal,
- uninstall,
- update,
- updateAll,
- runInvoke,
- resetPluginApi,
- getApi,
- finishInstall,
- callAction,
- callHook,
- serve,
- serveLogo
- }
|