Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: uninstall modules #229

Merged
merged 6 commits into from May 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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