此插件提供了页面:
@@ -117,7 +117,7 @@ const config = computed({
set: value => emit('update:modelValue', value),
})
-const env = computed(() => ctx.manager.getEnvInfo(current.value?.name))
+const env = computed(() => ctx.manager.getEnvInfo(current.value?.name)!)
const local = computed(() => data.value.packages[current.value?.name!])
const hint = computed(() => local.value.workspace ? '请检查插件源代码。' : '请联系插件作者并反馈此问题。')
diff --git a/plugins/manager/client/components/select.vue b/plugins/manager/client/components/select.vue
index 902028e..a1debbf 100644
--- a/plugins/manager/client/components/select.vue
+++ b/plugins/manager/client/components/select.vue
@@ -52,10 +52,8 @@ const dialogSelect = computed({
const keyword = ref('')
const input = ref()
-const filter = inject('plugin-select-filter', (local: LocalObject) => true)
-
const packages = computed(() => Object.values(ctx.manager.data.value.packages).filter((local) => {
- return local.package.name.includes(keyword.value.toLowerCase()) && filter(local)
+ return local.package.name.includes(keyword.value.toLowerCase())
}))
function joinName(name: string, base: string) {
@@ -64,7 +62,7 @@ function joinName(name: string, base: string) {
}
async function configure(name: string) {
- const parent = dialogSelect.value!.path
+ const parent = dialogSelect.value!.id
dialogSelect.value = undefined
keyword.value = ''
const id = await send('manager.config.create', {
diff --git a/plugins/manager/client/components/tree.vue b/plugins/manager/client/components/tree.vue
index 1303363..c083c0c 100644
--- a/plugins/manager/client/components/tree.vue
+++ b/plugins/manager/client/components/tree.vue
@@ -7,14 +7,13 @@
-import { computed, ref, onActivated, nextTick, watch } from 'vue'
+import { computed, ref, onActivated, nextTick, watch, VNodeRef } from 'vue'
import { useRoute } from 'vue-router'
import type { ElScrollbar, ElTree } from 'element-plus'
+import type { FilterNodeMethodFunction, TreeOptionProps } from 'element-plus/es/components/tree/src/tree.type'
+import type TreeNode from 'element-plus/es/components/tree/src/model/node'
import { send, useContext, useMenu } from '@cordisjs/client'
-import { Node } from '..'
+import { EntryData } from '../../src'
const props = defineProps<{
modelValue: string
@@ -58,7 +59,8 @@ const root = ref>()
const tree = ref>()
const keyword = ref('')
-function filterNode(value: string, data: Node) {
+const filterNode: FilterNodeMethodFunction = (value, data, node) => {
+ console.log('filter', value, data, node)
return data.name.toLowerCase().includes(keyword.value.toLowerCase())
}
@@ -83,77 +85,75 @@ onActivated(async () => {
isActivating.value = false
})
-function handleItemMount(itemEl: HTMLElement) {
+const handleItemMount: VNodeRef = (itemEl) => {
if (!itemEl || isActivating.value) return
activate()
}
-interface TreeNode {
- data: Node
- label?: string
- parent: TreeNode
- expanded: boolean
- isLeaf: boolean
- childNodes: TreeNode[]
+interface EntryNode extends Omit {
+ data: EntryData
+ parent: EntryNode
+ childNodes: EntryNode[]
}
-function getLabel(node: TreeNode) {
- if (node.data.name === 'group') {
- return '分组:' + (node.label || node.data.path)
+function getLabel(node: EntryNode) {
+ if (node.data.isGroup) {
+ return '分组:' + (node.data.label || node.data.id)
} else {
- return node.label || node.data.name || '待添加'
+ return node.data.label || node.data.name || '待添加'
}
}
-function allowDrag(node: TreeNode) {
- return node.data.path !== ''
+function allowDrag(node: EntryNode) {
+ return !!node.data.id
}
-function allowDrop(source: TreeNode, target: TreeNode, type: 'inner' | 'prev' | 'next') {
- if (type !== 'inner') {
- return target.data.path !== '' || type === 'next'
- }
- return target.data.id.startsWith('group:')
+function allowDrop(source: EntryNode, target: EntryNode, type: 'inner' | 'prev' | 'next') {
+ if (type !== 'inner') return true
+ return target.data.isGroup
}
-function handleClick(tree: Node, target: TreeNode, instance: any, event: MouseEvent) {
- emit('update:modelValue', tree.path)
+function handleClick(tree: EntryData, target: EntryNode, instance: any, event: MouseEvent) {
+ emit('update:modelValue', tree.id)
// el-tree will stop propagation,
// so we need to manually trigger the event
// so that context menu can be closed.
window.dispatchEvent(new MouseEvent(event.type, event))
}
-function handleExpand(data: Node, target: TreeNode, instance) {
+function handleExpand(data: EntryData, target: EntryNode, instance: any) {
send('manager.config.update', {
- id: data.path,
+ id: data.id,
collapse: null,
})
}
-function handleCollapse(data: Node, target: TreeNode, instance) {
+function handleCollapse(data: EntryData, target: EntryNode, instance: any) {
send('manager.config.update', {
- id: data.path,
+ id: data.id,
collapse: true,
})
}
-function handleDrop(source: TreeNode, target: TreeNode, position: 'before' | 'after' | 'inner', event: DragEvent) {
+function handleDrop(source: EntryNode, target: EntryNode, position: 'before' | 'after' | 'inner', event: DragEvent) {
const parent = position === 'inner' ? target : target.parent
- const index = parent.childNodes.findIndex(node => node.data.path === source.data.path)
+ const index = parent.childNodes.findIndex(node => node.data.id === source.data.id)
send('manager.config.transfer', {
id: source.data.id,
- parent: parent.data.path,
- index,
+ parent: parent.data.id,
+ position: index,
})
}
-function getClass(tree: Node) {
- const words: string[] = []
- if (tree.children) words.push('is-group')
- if (!tree.children && !(tree.name in ctx.manager.data.value.packages)) words.push('is-disabled')
- if (tree.path === props.modelValue) words.push('is-active')
- return words.join(' ')
+const optionProps: TreeOptionProps = {
+ class(tree: any, node) {
+ const data = tree as EntryData
+ const words: string[] = []
+ if (data.isGroup) words.push('is-group')
+ if (!data.isGroup && !(data.name in ctx.manager.data.value.packages)) words.push('is-disabled')
+ if (data.id === props.modelValue) words.push('is-active')
+ return words.join(' ')
+ },
}
watch(keyword, (val) => {
diff --git a/plugins/manager/client/index.ts b/plugins/manager/client/index.ts
index 6271f19..5c8de14 100644
--- a/plugins/manager/client/index.ts
+++ b/plugins/manager/client/index.ts
@@ -1,10 +1,9 @@
import { Context, Dict, router, ScopeStatus, send, Service } from '@cordisjs/client'
import { computed, defineComponent, h, Ref, ref, resolveComponent } from 'vue'
-import type { Entry } from '@cordisjs/loader'
+import type { Data, EntryData } from '../src'
import Settings from './components/index.vue'
import Forks from './components/forks.vue'
import Select from './components/select.vue'
-import type { Data } from '../src'
import './index.scss'
import './icons'
@@ -15,7 +14,7 @@ declare module '@cordisjs/client' {
}
interface ActionContext {
- 'config.tree': Node
+ 'config.tree': EntryData
}
}
@@ -25,14 +24,7 @@ export const coreDeps = [
'@cordisjs/plugin-server',
]
-export interface Node {
- id: string
- name: string
- path: string
- label?: string
- config?: any
- parent?: Node
- disabled?: boolean
+export interface Node extends EntryData {
children?: Node[]
}
@@ -57,36 +49,30 @@ export default class Manager extends Service {
optional: ['manager'],
}
- current = ref()
+ current = ref()
dialogFork = ref()
- dialogSelect = ref()
+ dialogSelect = ref()
plugins = computed(() => {
const expanded: string[] = []
const forks: Dict = {}
- const paths: Dict = {}
- const handle = (config: Entry.Options[]) => {
- return config.map(options => {
+ const entries: Dict = Object.fromEntries(this.data.value.entries.map(options => [options.id, options]))
+ const buildChildren = (parent: string | null) => this.data.value.entries
+ .filter(entry => entry.parent === parent)
+ .map((options) => {
const node: Node = {
- id: options.id,
- name: options.name,
- path: options.id,
- config: options.config,
+ ...options,
+ children: buildChildren(options.id),
}
- if (options.name === 'cordis/group') {
- node.children = handle(options.config)
+ forks[options.name] ||= []
+ forks[options.name].push(options.id)
+ if (options.isGroup && !options.collapse) {
+ expanded.push(options.id)
}
- if (!options.collapse && node.children) {
- expanded.push(node.path)
- }
- forks[node.name] ||= []
- forks[node.name].push(node.path)
- paths[node.path] = node
return node
})
- }
- const data = handle(this.data.value.config)
- return { data, forks, paths, expanded }
+ const data = buildChildren(null)
+ return { data, forks, entries, expanded }
})
type = computed(() => {
@@ -161,7 +147,7 @@ export default class Manager extends Service {
id: '.remove',
type: 'danger',
icon: 'delete',
- label: ({ config }) => config.tree?.children ? '移除分组' : '移除插件',
+ label: ({ config }) => config.tree?.isGroup ? '移除分组' : '移除插件',
}, {
id: '@separator',
}, {
@@ -200,25 +186,25 @@ export default class Manager extends Service {
}
}
- async remove(tree: string | Node) {
- if (typeof tree === 'string') {
- const forks = this.plugins.value.forks[tree]
+ async remove(options: string | EntryData) {
+ if (typeof options === 'string') {
+ const forks = this.plugins.value.forks[options]
for (const id of forks) {
- const tree = this.plugins.value.paths[id]
- await send('manager.config.remove', { id: tree.id })
+ const options = this.plugins.value.entries[id]
+ await send('manager.config.remove', { id: options.id })
}
} else {
- await router.replace('/plugins/' + tree.parent!.path)
- await send('manager.config.remove', { id: tree.id })
+ await router.replace('/plugins/' + (options.parent ?? ''))
+ await send('manager.config.remove', { id: options.id })
}
}
get(name: string) {
- return this.plugins.value.forks[name]?.map(id => this.plugins.value.paths[id])
+ return this.plugins.value.forks[name]?.map(id => this.plugins.value.entries[id])
}
- getStatus(tree: Node) {
- switch (this.data.value.packages[tree.name]?.runtime?.forks?.[tree.path]?.status) {
+ getStatus(data: EntryData) {
+ switch (this.data.value.packages[data.name]?.runtime?.forks?.[data.id]?.status) {
case ScopeStatus.PENDING: return 'pending'
case ScopeStatus.LOADING: return 'loading'
case ScopeStatus.ACTIVE: return 'active'
@@ -244,6 +230,7 @@ export default class Manager extends Service {
// check peer dependencies
for (const name in local.package.peerDependencies ?? {}) {
+ // FIXME
if (!name.includes('@cordisjs/plugin-') && !name.includes('cordis-plugin-')) continue
if (coreDeps.includes(name)) continue
const required = !local.package.peerDependenciesMeta?.[name]?.optional
@@ -280,8 +267,8 @@ export default class Manager extends Service {
return result
}
- hasCoreDeps(tree: Node) {
- if (coreDeps.includes(tree.name)) return true
- if (tree.children) return tree.children.some(node => this.hasCoreDeps(node))
+ hasCoreDeps(data: EntryData) {
+ if (coreDeps.includes(data.name)) return true
+ return this.data.value.entries.some(entry => entry.parent === data.id && this.hasCoreDeps(entry))
}
}
diff --git a/plugins/manager/src/index.ts b/plugins/manager/src/index.ts
index 98690c2..4f5e3db 100644
--- a/plugins/manager/src/index.ts
+++ b/plugins/manager/src/index.ts
@@ -1,7 +1,6 @@
import { Context } from 'cordis'
import { LocalScanner } from '@cordisjs/registry'
import { Manager } from './shared'
-import { pathToFileURL } from 'url'
export * from './shared'
@@ -9,10 +8,10 @@ export default class NodeManager extends Manager {
scanner = new LocalScanner(this.ctx.baseDir, {
onSuccess: async (object) => {
const { name } = object.package
- const { internal, filename } = this.ctx.loader
+ const { internal, url: parentURL } = this.ctx.loader
if (!internal) return
try {
- const { url } = await internal!.resolve(name, pathToFileURL(filename).href, {})
+ const { url } = await internal!.resolve(name, parentURL, {})
if (internal?.loadCache.has(url)) {
object.runtime = await this.parseExports(name)
}
diff --git a/plugins/manager/src/shared.ts b/plugins/manager/src/shared.ts
index a7406ee..093e0e2 100644
--- a/plugins/manager/src/shared.ts
+++ b/plugins/manager/src/shared.ts
@@ -8,7 +8,7 @@ import {} from '@cordisjs/plugin-hmr'
declare module '@cordisjs/loader' {
namespace Entry {
interface Options {
- label?: string | Dict | null
+ label?: string | null
collapse?: boolean | null
}
}
@@ -16,7 +16,7 @@ declare module '@cordisjs/loader' {
declare module '@cordisjs/plugin-webui' {
interface Events {
- 'manager.config.list'(): LoaderEntry.Options[]
+ 'manager.config.list'(): EntryData[]
'manager.config.create'(options: Omit & EntryLocation): Promise
'manager.config.update'(options: Omit): void
'manager.config.remove'(options: { id: string }): void
@@ -33,8 +33,12 @@ declare module '@cordisjs/registry' {
}
}
+export interface EntryData extends LoaderEntry.Options, Required {
+ isGroup?: boolean
+}
+
export interface Data {
- config: LoaderEntry.Options[]
+ entries: EntryData[]
packages: Dict
services: Dict
}
@@ -53,9 +57,9 @@ export interface RuntimeData {
}>
}
-interface EntryLocation {
- parent?: string
- index?: number
+export interface EntryLocation {
+ parent?: string | null
+ position?: number
}
export abstract class Manager extends Service {
@@ -70,14 +74,16 @@ export abstract class Manager extends Service {
constructor(public ctx: Context) {
super(ctx, 'manager', true)
-
- if (!ctx.loader?.writable) {
- throw new Error('@cordisjs/plugin-manager is only available for json/yaml config file')
- }
}
- getConfig() {
- return this.ctx.loader.config
+ getEntries() {
+ return Object.values(this.ctx.loader.entries).map((entry) => ({
+ ...entry.options,
+ config: entry.children?.data === entry.options.config ? undefined : entry.options.config,
+ parent: entry.parent.ctx.scope.entry?.options.id ?? null,
+ position: entry.parent.data.indexOf(entry.options),
+ isGroup: !!entry.children,
+ }))
}
getServices() {
@@ -88,7 +94,7 @@ export abstract class Manager extends Service {
if (!(instance instanceof Object)) continue
const ctx: Context = Reflect.getOwnPropertyDescriptor(instance, Context.current)?.value
if (!ctx) continue
- result[name] = this.ctx.loader.paths(ctx.scope)
+ result[name] = this.ctx.loader.locate(ctx)
}
return result
}
@@ -102,13 +108,13 @@ export abstract class Manager extends Service {
import.meta.resolve('../dist/style.css'),
],
}, () => (this.getPackages(), {
- config: this.getConfig(),
+ entries: this.getEntries(),
packages: this.packages,
services: this.getServices(),
}))
ctx.on('config', ctx.debounce(() => {
- this.entry?.patch({ config: this.getConfig() })
+ this.entry?.patch({ entries: this.getEntries() })
}, 0))
ctx.on('internal/service', ctx.debounce(() => {
@@ -124,12 +130,12 @@ export abstract class Manager extends Service {
})
ctx.webui.addListener('manager.config.list', () => {
- return this.getConfig()
+ return this.getEntries()
})
ctx.webui.addListener('manager.config.create', (options) => {
- const { parent, index, ...rest } = options
- return ctx.loader.create(rest, parent, index)
+ const { parent, position, ...rest } = options
+ return ctx.loader.create(rest, parent, position)
})
ctx.webui.addListener('manager.config.update', (options) => {
@@ -142,8 +148,8 @@ export abstract class Manager extends Service {
})
ctx.webui.addListener('manager.config.transfer', (options) => {
- const { id, parent, index } = options
- return ctx.loader.transfer(id, parent ?? '', index)
+ const { id, parent, position } = options
+ return ctx.loader.transfer(id, parent ?? '', position)
})
ctx.webui.addListener('manager.package.list', async () => {
diff --git a/plugins/notifier/src/index.ts b/plugins/notifier/src/index.ts
index cf6a1ec..0403799 100644
--- a/plugins/notifier/src/index.ts
+++ b/plugins/notifier/src/index.ts
@@ -1,6 +1,7 @@
import { Context, Schema, Service } from 'cordis'
import { Dict, isNullable, remove } from 'cosmokit'
import { h } from '@cordisjs/element'
+import {} from '@cordisjs/loader'
import type { Entry } from '@cordisjs/plugin-webui'
declare module 'cordis' {
@@ -70,7 +71,7 @@ export class Notifier {
return {
...this.options,
content: this.options.content.join(''),
- paths: this.ctx.get('loader')?.paths(this.ctx.scope),
+ paths: this.ctx.get('loader')?.locate(),
}
}
}
diff --git a/plugins/webui/src/shared/entry.ts b/plugins/webui/src/shared/entry.ts
index e92051a..ad49152 100644
--- a/plugins/webui/src/shared/entry.ts
+++ b/plugins/webui/src/shared/entry.ts
@@ -51,7 +51,7 @@ export class Entry {
toJSON(): Entry.Data {
return {
files: this.ctx.webui.resolveEntry(this.files, this.id),
- paths: this.ctx.get('loader')?.paths(this.ctx.scope),
+ paths: this.ctx.get('loader')?.locate(),
data: this.data?.(),
}
}