Skip to content

Commit

Permalink
feat(config): support on-demand plugin load
Browse files Browse the repository at this point in the history
  • Loading branch information
shigma committed May 15, 2023
1 parent b16cb6c commit 5fd4c7a
Show file tree
Hide file tree
Showing 21 changed files with 174 additions and 103 deletions.
2 changes: 1 addition & 1 deletion plugins/config/client/components/global.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<template>
<k-form :schema="store.packages[''].schema" :initial="current.config" v-model="config"></k-form>
<k-form :schema="store.packages[''].runtime.schema" :initial="current.config" v-model="config"></k-form>
</template>

<script lang="ts" setup>
Expand Down
40 changes: 14 additions & 26 deletions plugins/config/client/components/plugin.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,26 @@
<template v-if="name">
<k-slot name="market-settings" :data="data"></k-slot>

<!-- schema -->
<k-comment v-if="!local.schema" type="warning">
<k-comment v-if="!local.runtime">
<p>正在加载插件配置……</p>
</k-comment>
<k-comment v-else-if="!local.runtime.schema" type="warning">
<p>此插件未声明配置项,这可能并非预期行为{{ hint }}。</p>
</k-comment>
<k-form :schema="local.schema" :initial="current.config" v-model="config">
<k-form v-else :schema="local.runtime.schema" :initial="current.config" v-model="config">
<template #hint>{{ hint }}</template>
</k-form>
</template>

<k-comment v-else-if="current.label" type="error">
<p>此插件尚未安装,<span class="link" @click.stop="gotoMarket">点击前往插件市场</span>。</p>
</k-comment>
<template v-else-if="current.label">
<k-slot name="plugin-missing"></k-slot>
</template>
</template>

<script lang="ts" setup>
import { store, router } from '@koishijs/client'
import { computed, provide } from 'vue'
import { store, send } from '@koishijs/client'
import { computed, provide, watch } from 'vue'
import { SettingsData, Tree } from './utils'
const props = defineProps<{
Expand Down Expand Up @@ -48,12 +50,12 @@ const name = computed(() => {
})
const local = computed(() => store.packages[name.value])
const remote = computed(() => store.market?.data[name.value])
const hint = computed(() => local.value.workspace ? ',请检查源代码' : ',请联系插件作者')
function gotoMarket() {
router.push('/market?keyword=' + props.current.label)
}
watch(local, (value) => {
if (!value || value.runtime) return
send('config/request-runtime', value.name)
})
provide('manager.settings.local', local)
provide('manager.settings.config', config)
Expand All @@ -62,22 +64,8 @@ provide('manager.settings.current', computed(() => props.current))
const data = computed<SettingsData>(() => ({
name: name.value,
local: local.value,
remote: remote.value,
config: config.value,
current: props.current,
}))
</script>

<style lang="scss">
.plugin-view {
span.link {
&:hover {
cursor: pointer;
text-decoration: underline;
}
}
}
</style>
2 changes: 1 addition & 1 deletion plugins/config/client/components/slots/modifier.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<template>
<k-modifier v-if="data.local.filter !== false" v-model="config"></k-modifier>
<k-modifier v-if="data.local.runtime.filter !== false" v-model="config"></k-modifier>
</template>

<script lang="ts" setup>
Expand Down
2 changes: 1 addition & 1 deletion plugins/config/client/components/slots/usage.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<template>
<k-markdown unsafe class="usage" v-if="data.local.usage" :source="data.local.usage"></k-markdown>
<k-markdown unsafe class="usage" v-if="data.local.runtime?.usage" :source="data.local.runtime?.usage"></k-markdown>
</template>

<script lang="ts" setup>
Expand Down
8 changes: 3 additions & 5 deletions plugins/config/client/components/utils.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
import { Dict } from 'koishi'
import { computed, ref } from 'vue'
import { router, send, store } from '@koishijs/client'
import { PackageProvider } from '@koishijs/plugin-market'
import { AnalyzedPackage } from '@koishijs/registry'
import { PackageProvider } from '@koishijs/plugin-config'

export interface SettingsData {
name: string
local: PackageProvider.Data
remote: AnalyzedPackage
config: any
current: Tree
}
Expand Down Expand Up @@ -61,7 +59,7 @@ function getEnvInfo(name: string) {
if (!name.includes('@koishijs/plugin-') && !name.includes('koishi-plugin-')) continue
if (coreDeps.includes(name)) continue
const required = !local.peerDependenciesMeta?.[name]?.optional
const active = !!store.packages[name]?.id
const active = !!store.packages[name]?.runtime.id
result.peer[name] = { required, active }
for (const service of getImplements(name)) {
services.add(service)
Expand All @@ -83,7 +81,7 @@ function getEnvInfo(name: string) {
}

// check reusability
if (local.id && !local.forkable) {
if (local.runtime?.id && !local.runtime?.forkable) {
result.warning = true
}

Expand Down
2 changes: 2 additions & 0 deletions plugins/config/client/icons/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import AddGroup from './add-group.vue'
import AddPlugin from './add-plugin.vue'
import TrashCan from './trash-can.vue'
import Check from './check.vue'
import Plugin from './plugin.vue'
import Play from './play.vue'
import Stop from './stop.vue'
import Save from './save.vue'

icons.register('activity:plugin', Plugin)
icons.register('add-group', AddGroup)
icons.register('add-plugin', AddPlugin)
icons.register('trash-can', TrashCan)
Expand Down
File renamed without changes.
19 changes: 6 additions & 13 deletions plugins/config/src/browser/packages.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,6 @@
import { Context, pick } from 'koishi'
import { MarketResult } from '@koishijs/registry'
import * as shared from '../shared'

declare module '@koishijs/loader' {
interface Loader {
market: MarketResult
}
}

export class PackageProvider extends shared.PackageProvider {
async getManifest(name: string) {
return this.ctx.loader.market.objects.find(item => {
Expand All @@ -29,19 +22,19 @@ export class PackageProvider extends shared.PackageProvider {
result.peerDependencies = { ...data.versions[data.version].peerDependencies }
result.peerDependenciesMeta = { ...data.versions[data.version].peerDependenciesMeta }
if (!result.portable) return
const exports = await this.ctx.loader.resolvePlugin(data.shortname)
result.schema = exports?.Config || exports?.schema
result.usage = exports?.usage
const runtime = this.ctx.registry.get(exports)
if (runtime) this.parseRuntime(runtime, result)
result.runtime = await this.parseExports(data.name, () => {
return this.ctx.loader.resolvePlugin(data.shortname)
})
return result
}))

// add app config
packages.unshift({
name: '',
shortname: '',
schema: Context.Config,
runtime: {
schema: Context.Config,
},
})

return Object.fromEntries(packages.filter(x => x).map(data => [data.name, data]))
Expand Down
58 changes: 30 additions & 28 deletions plugins/config/src/node/packages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { unwrapExports } from '@koishijs/loader'
import { loadManifest } from './utils'
import * as shared from '../shared'

const logger = new Logger('market')
const logger = new Logger('config')

/** require without affecting the dependency tree */
function getExports(id: string) {
Expand All @@ -30,13 +30,24 @@ export class PackageProvider extends shared.PackageProvider {
cache: Dict<PackageProvider.Data> = {}
task: Promise<void>

constructor(ctx: Context) {
super(ctx)

ctx.console.addListener('config/request-runtime', async (name) => {
const entry = require.resolve(name)
const result = this.cache[entry]
if (!result || result.runtime) return
result.runtime = await this.parseExports(name, () => getExports(name))
this.refresh()
})
}

update(state: EffectScope) {
const entry = Object.keys(require.cache).find((key) => {
return unwrapExports(require.cache[key].exports) === state.runtime.plugin
})
if (!this.cache[entry]) return
const data = this.cache[entry]
this.parseRuntime(state.runtime, data)
if (!this.cache[entry]?.runtime) return
this.parseRuntime(state, this.cache[entry].runtime)
this.refresh()
}

Expand All @@ -62,7 +73,9 @@ export class PackageProvider extends shared.PackageProvider {
packages.unshift({
name: '',
shortname: '',
schema: Context.Config,
runtime: {
schema: Context.Config,
},
})

return Object.fromEntries(packages.filter(x => x).map(data => [data.name, data]))
Expand All @@ -72,34 +85,33 @@ export class PackageProvider extends shared.PackageProvider {
const base = baseDir + '/node_modules'
const files = await fsp.readdir(base).catch(() => [])
for (const name of files) {
const base2 = base + '/' + name
if (name.startsWith('@')) {
if (name.startsWith('koishi-plugin-')) {
this.loadPackage(name)
} else if (name.startsWith('@')) {
const base2 = base + '/' + name
const files = await fsp.readdir(base2).catch(() => [])
for (const name2 of files) {
if (name === '@koishijs' && name2.startsWith('plugin-') || name2.startsWith('koishi-plugin-')) {
this.loadPackage(name + '/' + name2)
}
}
} else {
if (name.startsWith('koishi-plugin-')) {
this.loadPackage(name)
}
}
}
}

private loadPackage(name: string) {
private async loadPackage(name: string) {
try {
// require.resolve(name) may be different from require.resolve(path)
// because tsconfig-paths may resolve the path differently
this.cache[require.resolve(name)] = this.parsePackage(name)
const entry = require.resolve(name)
this.cache[entry] = await this.parsePackage(name, entry)
} catch (error) {
logger.warn('failed to parse %c', name)
logger.warn('failed to resolve %c', name)
logger.warn(error)
}
}

private parsePackage(name: string) {
private async parsePackage(name: string, entry: string) {
const data = loadManifest(name)
const result = pick(data, [
'name',
Expand All @@ -114,19 +126,9 @@ export class PackageProvider extends shared.PackageProvider {
result.peerDependencies = { ...data.peerDependencies }
result.peerDependenciesMeta = { ...data.peerDependenciesMeta }

// check schema
const exports = getExports(name)
result.schema = exports?.Config || exports?.schema
result.usage = exports?.usage
result.filter = exports?.filter

// check plugin state
const runtime = this.ctx.registry.get(exports)
if (runtime) this.parseRuntime(runtime, result)

// make sure that result can be serialized into json
JSON.stringify(result)

if (require.resolve[entry]) {
result.runtime = await this.parseExports(name, () => unwrapExports(require.cache[entry].exports))
}
return result
}

Expand Down
59 changes: 49 additions & 10 deletions plugins/config/src/shared/packages.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,22 @@
import { Context, Dict, EffectScope, MainScope, Schema } from 'koishi'
import { Context, Dict, EffectScope, Logger, Schema } from 'koishi'
import { DataService } from '@koishijs/plugin-console'
import { Manifest, PackageJson } from '@koishijs/registry'
import { Manifest, MarketResult, PackageJson } from '@koishijs/registry'
import { debounce } from 'throttle-debounce'

declare module '@koishijs/loader' {
interface Loader {
market: MarketResult
}
}

declare module '@koishijs/plugin-console' {
interface Events {
'config/request-runtime'(name: string): void
}
}

const logger = new Logger('config')

export abstract class PackageProvider extends DataService<Dict<PackageProvider.Data>> {
constructor(ctx: Context) {
super(ctx, 'packages', { authority: 4 })
Expand All @@ -18,22 +32,47 @@ export abstract class PackageProvider extends DataService<Dict<PackageProvider.D
this.refresh()
}

parseRuntime(runtime: MainScope, result: PackageProvider.Data) {
result.id = runtime.uid
result.forkable = runtime.isForkable
parseRuntime(state: EffectScope, result: PackageProvider.RuntimeData) {
result.id = state.runtime.uid
result.forkable = state.runtime.isForkable
}

async parseExports(name: string, callback: () => Promise<any>) {
try {
const exports = await callback()
const result: PackageProvider.RuntimeData = {}
result.schema = exports?.Config || exports?.schema
result.usage = exports?.usage

// make sure that result can be serialized into json
JSON.stringify(result)

const runtime = this.ctx.registry.get(exports)
if (runtime) this.parseRuntime(runtime, result)
return result
} catch (error) {
logger.warn('failed to load %c', name)
logger.warn(error)
return { failed: true }
}
}
}

export namespace PackageProvider {
export interface Data extends Partial<PackageJson> {
id?: number
runtime?: RuntimeData
portable?: boolean
forkable?: boolean
shortname?: string
schema?: Schema
usage?: string
filter?: boolean
workspace?: boolean
manifest?: Manifest
}

export interface RuntimeData {
id?: number
filter?: boolean
forkable?: boolean
schema?: Schema
usage?: string
failed?: boolean
}
}
2 changes: 0 additions & 2 deletions plugins/config/src/shared/writer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,6 @@ function dropKey(plugins: {}, name: string) {
}

export class ConfigWriter extends DataService<Context.Config> {
static using = ['console.packages']

protected loader: Loader
protected plugins: {}

Expand Down
Loading

0 comments on commit 5fd4c7a

Please sign in to comment.