Skip to content

Commit

Permalink
fix: uninstall modules (#229)
Browse files Browse the repository at this point in the history
Co-authored-by: Anthony Fu <anthonyfu117@hotmail.com>
  • Loading branch information
arashsheyda and antfu committed May 15, 2023
1 parent 3b99477 commit f7db6a2
Show file tree
Hide file tree
Showing 11 changed files with 247 additions and 121 deletions.
11 changes: 11 additions & 0 deletions packages/devtools-kit/src/_types/integrations.ts
Expand Up @@ -81,6 +81,17 @@ export interface BasicModuleInfo {
}
}

export interface InstalledModuleInfo {
name?: string
isPackageModule: boolean
isUninstallable: boolean
info?: ModuleStaticInfo
entryPath?: string
meta?: {
name?: string
}
}

export interface ModuleStaticInfo {
name: string
description: string
Expand Down
1 change: 1 addition & 0 deletions packages/devtools-kit/src/_types/rpc.ts
Expand Up @@ -55,6 +55,7 @@ export interface ServerFunctions {
openInEditor(filepath: string): Promise<boolean>
restartNuxt(hard?: boolean): Promise<void>
installNuxtModule(name: string, dry?: boolean): Promise<InstallModuleReturn>
uninstallNuxtModule(name: string, dry?: boolean): Promise<InstallModuleReturn>
}

export interface ClientFunctions {
Expand Down
2 changes: 1 addition & 1 deletion packages/devtools-ui-kit/src/components/NDropdown.vue
Expand Up @@ -15,7 +15,7 @@ onClickOutside(el, () => {

<template>
<div ref="el" class="relative">
<slot name="trigger" :enabled="enabled" @click="enabled = !enabled">
<slot name="trigger" :enabled="enabled" :click="() => enabled = !enabled">
<NButton @click="enabled = !enabled">
Dropdown
</NButton>
Expand Down
52 changes: 52 additions & 0 deletions packages/devtools/client/components/ModuleActionDialog.vue
@@ -0,0 +1,52 @@
<script setup lang="ts">
import { ModuleDialog } from '../composables/state'
const config = useServerConfig()
const openInEditor = useOpenInEditor()
</script>

<template>
<ModuleDialog v-slot="{ resolve, args }">
<NDialog :model-value="true" @close="resolve(false)">
<ModuleItemBase :mod="{}" :info="args[0]" border="none" w-150 n-panel-grids />
<div flex="~ col gap-2" w-150 p4 border="t base">
<h2 text-xl :class="args[2] === 'install' ? 'text-primary' : 'text-red'">
<span capitalize>{{ args[2] }}</span> <code>{{ args[0].name }}</code>?
</h2>

<p op50>
Following command will be executed in your terminal:
</p>
<NCodeBlock :code="args[1].commands.join(' ')" lang="bash" px4 py2 border="~ base rounded" :lines="false" />

<p op50>
Then your <NLink role="button" n="primary" @click="openInEditor(config?._nuxtConfigFile)" v-text="'Nuxt config'" /> will be updated as:
</p>
<CodeDiff
:from="args[1].configOriginal"
:to="args[1].configGenerated"
max-h-80 of-auto py2 border="~ base rounded"
lang="ts"
/>
<p>
<span op50>After that, Nuxt will </span><span text-orange>restart automatically</span>.
</p>
<div flex="~ gap-3" mt2 justify-end>
<NTip n="sm purple" flex-auto icon="carbon-chemistry">
Experimental. Backup your project first.
</NTip>
<NButton @click="resolve(false)">
Cancel
</NButton>
<NButton n="solid" capitalize :class="args[2] === 'install' ? 'n-primary' : 'n-red'" @click="resolve(true)">
{{ args[2] }}
</NButton>
</div>
</div>
</NDialog>
</ModuleDialog>
</template>
81 changes: 5 additions & 76 deletions packages/devtools/client/components/ModuleInstallList.vue
@@ -1,17 +1,12 @@
<script setup lang="ts">
// @ts-expect-error missing types
import { RecycleScroller } from 'vue-virtual-scroller'
import type { InstallModuleReturn, ModuleStaticInfo } from '@nuxt/devtools-kit/types'
import Fuse from 'fuse.js'
const Dialog = createTemplatePromise<boolean, [info: ModuleStaticInfo, result: InstallModuleReturn]>()
const collection = await useModulesInfo()
const nuxt3only = collection.filter(i => i.compatibility.nuxt.includes('^3'))
const collection = useModulesList()
const config = useServerConfig()
const router = useRouter()
const search = ref('')
const fuse = computed(() => new Fuse(nuxt3only, {
const fuse = computed(() => new Fuse(collection.value || [], {
keys: [
'name',
'description',
Expand All @@ -22,32 +17,17 @@ const fuse = computed(() => new Fuse(nuxt3only, {
const items = computed(() => {
if (!search.value)
return nuxt3only
return collection.value
return fuse.value.search(search.value).map(r => r.item)
})
async function install(item: ModuleStaticInfo) {
const result = await rpc.installNuxtModule(item.npm, true)
if (!result.commands)
return
if (!await Dialog.start(item, result))
return
router.push(`/modules/terminals?id=${encodeURIComponent(result.processId)}`)
await rpc.installNuxtModule(item.npm, false)
}
const openInEditor = useOpenInEditor()
</script>

<template>
<div h-full flex="~ col gap-4">
<NIconTitle
mx6 mt6
text-xl op75
icon="i-carbon-3d-mpr-toggle"
icon="i-carbon-intent-request-create"
text="Install Module"
/>

Expand All @@ -66,59 +46,8 @@ const openInEditor = useOpenInEditor()
:item-size="160"
key-field="name"
>
<ModuleItemBase
:mod="{}"
role="button"
:info="item"
mb2 h-full class="hover:bg-active!"
:compact="true"
@click="install(item)"
/>
<ModuleItemInstall :item="item" />
</RecycleScroller>
</div>
</div>

<Dialog v-slot="{ resolve, args }">
<NDialog :model-value="true" @close="resolve(false)">
<ModuleItemBase :mod="{}" :info="args[0]" border="none" w-150 n-panel-grids />
<div flex="~ col gap-2" w-150 p4 border="t base">
<h2 text-xl>
Installing <span capitalize text-primary>{{ args[0].name }}</span> module?
</h2>

<p op50>
Following command will be executed in your terminal:
</p>
<NCodeBlock :code="args[1].commands.join(' ')" lang="bash" px4 py2 border="~ base rounded" :lines="false" />

<p op50>
Then your <NLink role="button" n="primary" @click="openInEditor(config?._nuxtConfigFile)" v-text="'Nuxt config'" /> will be updated as:
</p>
<CodeDiff
:from="args[1].configOriginal"
:to="args[1].configGenerated"
max-h-80 of-auto py2 border="~ base rounded"
lang="ts"
/>
<p>
<span op50>After module installed, Nuxt will </span><span text-orange>restart automatically</span>.
</p>
<div flex="~ gap-3" mt2 justify-end>
<NTip n="sm purple" flex-auto icon="carbon-chemistry">
Experimental. Make sure to backup your project.
</NTip>
<NButton @click="resolve(false)">
Cancel
</NButton>
<NButton n="solid primary" @click="resolve(true)">
Install
</NButton>
</div>
</div>
</NDialog>
</Dialog>
</template>
7 changes: 3 additions & 4 deletions packages/devtools/client/components/ModuleItem.vue
@@ -1,8 +1,8 @@
<script setup lang="ts">
import type { BasicModuleInfo } from '../../src/types'
import type { InstalledModuleInfo } from '../../src/types'
const props = defineProps<{
mod: BasicModuleInfo
mod: InstalledModuleInfo
}>()
const config = useServerConfig()
Expand All @@ -19,8 +19,7 @@ const name = computed(() => {
}
return ''
})
const collection = await useModulesInfo()
const staticInfo = computed(() => (collection || []).find?.(i => i.npm === name.value || i.name === name.value))
const staticInfo = computed(() => props.mod.info)
const data = computed(() => ({
name,
...props.mod?.meta,
Expand Down
13 changes: 9 additions & 4 deletions packages/devtools/client/components/ModuleItemBase.vue
Expand Up @@ -38,9 +38,7 @@ const openInEditor = useOpenInEditor()
<template>
<NCard p4 flex="~ gap2">
<div flex="~ col gap2" flex-auto of-hidden px1>
<div
of-hidden text-ellipsis ws-nowrap text-lg
>
<div gap-1t flex items-center text-ellipsis ws-nowrap text-lg>
<NuxtLink
v-if="isPackageModule"
:to="npmBase + (data.npm || data.name)"
Expand All @@ -60,6 +58,7 @@ const openInEditor = useOpenInEditor()
<span v-else>
{{ data.name }}
</span>
<slot name="badge" />
</div>

<div
Expand Down Expand Up @@ -97,7 +96,7 @@ const openInEditor = useOpenInEditor()

<slot name="items" />
</div>
<div flex="~ col">
<div flex="~ col" items-end>
<div
v-if="data.icon || isPackageModule"

Expand All @@ -117,6 +116,12 @@ const openInEditor = useOpenInEditor()
<img :src="avatarBase + m.github" h-6 w-6 rounded-full>
</NuxtLink>
</div>
<template v-if="$slots.actions">
<div flex-auto />
<div flex justify-end>
<slot name="actions" />
</div>
</template>
</div>
</NCard>
</template>
38 changes: 38 additions & 0 deletions packages/devtools/client/components/ModuleItemInstall.vue
@@ -0,0 +1,38 @@
<script setup lang="ts">
import type { ModuleStaticInfo } from '../../src/types'
const props = defineProps<{
item: ModuleStaticInfo
}>()
const installedModules = useInstalledModules()
const installedInfo = computed(() => installedModules.value.find(i => i.name === props.item.npm))
const isInstalled = computed(() => installedInfo.value && installedInfo.value.isPackageModule)
const isUninstallable = computed(() => installedInfo.value && installedInfo.value.isPackageModule && installedInfo.value.isUninstallable)
</script>

<template>
<ModuleItemBase
:mod="{}"
:role="isInstalled ? '' : 'button'"
:info="item"
mb2 h-full
:class="isInstalled ? 'border-dashed op75' : 'hover:bg-active!'"
:compact="true"
@click="isInstalled ? null : useModuleAction(item, 'install')"
>
<template v-if="isInstalled" #badge>
<Badge bg-green-400:10 text-green-400>
Installed
</Badge>
<NDropdown v-if="isUninstallable" n="sm green">
<template #trigger="{ click }">
<NIconButton icon="carbon-overflow-menu-vertical" @click="click()" />
</template>
<NButton icon="carbon-trash-can" n="red" @click="useModuleAction(item, 'uninstall')">
Uninstall
</NButton>
</NDropdown>
</template>
</ModuleItemBase>
</template>
76 changes: 69 additions & 7 deletions packages/devtools/client/composables/state.ts
Expand Up @@ -2,15 +2,77 @@ import type { Component } from 'nuxt/schema'
import { $fetch } from 'ofetch'
import type { Ref } from 'vue'
import { objectPick } from '@antfu/utils'
import type { HookInfo, ModuleBuiltinTab, ModuleCustomTab, ModuleStaticInfo, RouteInfo, TabCategory } from '../../src/types'
import type { HookInfo, InstallModuleReturn, InstalledModuleInfo, ModuleBuiltinTab, ModuleCustomTab, ModuleStaticInfo, RouteInfo, TabCategory } from '../../src/types'

const ignoredModules = [
'pages',
'meta',
'components',
'imports',
'nuxt-config-schema',
'@nuxt/devtools',
'@nuxt/telemetry',
]

export function useModulesList() {
return useAsyncData('modules-list', async () => {
const modules = await $fetch<ModuleStaticInfo[]>('https://cdn.jsdelivr.net/npm/@nuxt/modules@latest/modules.json')
return modules
.filter((m: ModuleStaticInfo) => !ignoredModules.includes(m.npm) && m.compatibility.nuxt.includes('^3'))
}).data
}

let modules: Promise<ModuleStaticInfo[]> | undefined
export function useInstalledModules() {
return useState('installed-modules', () => {
const config = useServerConfig()
const modules = useModulesList()

return computed(() => (config.value?._installedModules || [])
.map((mod): InstalledModuleInfo => {
const isPackageModule = mod.entryPath && isNodeModulePath(mod.entryPath)
const name = mod.meta?.name
? mod.meta?.name
: mod.entryPath
? isPackageModule
? getModuleNameFromPath(mod.entryPath)
: config.value?.rootDir
? parseReadablePath(mod.entryPath, config.value?.rootDir).path
: undefined
: undefined

const isUninstallable = config.value?.modules?.includes(name)
const info = modules.value?.find(m => m.npm === name)

export async function useModulesInfo() {
if (modules)
return modules
modules = $fetch('https://cdn.jsdelivr.net/npm/@nuxt/modules@latest/modules.json')
return modules
return {
name,
isPackageModule,
isUninstallable,
info,
...mod,
}
})
.filter(i => !i.name || !ignoredModules.includes(i.name)),
)
})
}

type ModuleActionType = 'install' | 'uninstall'

export const ModuleDialog = createTemplatePromise<boolean, [info: ModuleStaticInfo, result: InstallModuleReturn, type: ModuleActionType]>()

export async function useModuleAction(item: ModuleStaticInfo, type: ModuleActionType) {
const router = useRouter()
const method = type === 'install' ? rpc.installNuxtModule : rpc.uninstallNuxtModule
const result = await method(item.npm, true)

if (!result.commands)
return

if (!await ModuleDialog.start(item, result, type))
return

router.push(`/modules/terminals?id=${encodeURIComponent(result.processId)}`)
await method(item.npm, false)
}

export function useComponents() {
Expand Down

0 comments on commit f7db6a2

Please sign in to comment.