mirror of
https://github.com/vuejs/vue-cli.git
synced 2026-05-01 09:21:03 -05:00
684 lines
18 KiB
JavaScript
684 lines
18 KiB
JavaScript
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
|
|
}
|