Skip to content

Commit

Permalink
feat(config): refactor server-side code
Browse files Browse the repository at this point in the history
  • Loading branch information
shigma committed Apr 28, 2024
1 parent c80a278 commit c453740
Show file tree
Hide file tree
Showing 11 changed files with 277 additions and 390 deletions.
16 changes: 16 additions & 0 deletions plugins/config/src/browser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { SearchResult } from '@cordisjs/registry'
import BaseManager from './shared'

export * from './shared'

declare module '@cordisjs/loader' {
interface Loader {
market: SearchResult
}
}

export default class BrowserManager extends BaseManager {
async collect(forced: boolean) {
return this.ctx.loader.market.objects
}
}
26 changes: 0 additions & 26 deletions plugins/config/src/browser/index.ts

This file was deleted.

7 changes: 0 additions & 7 deletions plugins/config/src/browser/packages.ts

This file was deleted.

41 changes: 39 additions & 2 deletions plugins/config/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,39 @@
// placeholder file, do not modify
export * from './node'
import { LocalScanner } from '@cordisjs/registry'
import BaseManager from './shared'

export * from './shared'

class NodeScanner extends LocalScanner {
constructor(private manager: NodeManager) {
super(manager.ctx.baseDir)
}

async onError(error: any, name: string) {
this.manager.ctx.logger.warn('failed to resolve %c', name)
this.manager.ctx.logger.warn(error)
}

async parsePackage(name: string) {
const result = await super.parsePackage(name)
try {
// require.resolve(name) may be different from require.resolve(path)
// because tsconfig-paths may resolve the path differently
const entry = require.resolve(name)
if (require.cache[entry]) {
this.manager.cache[name] = await this.manager.parseExports(name)
}
} catch (error) {
this.onError(error, name)
}
return result
}
}

export default class NodeManager extends BaseManager {
scanner = new NodeScanner(this)

async collect(forced: boolean) {
await this.scanner.collect(forced)
return this.scanner.objects
}
}
28 changes: 0 additions & 28 deletions plugins/config/src/node/index.ts

This file was deleted.

40 changes: 0 additions & 40 deletions plugins/config/src/node/packages.ts

This file was deleted.

222 changes: 222 additions & 0 deletions plugins/config/src/shared.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
import { Context, MainScope, Plugin, Schema, ScopeStatus, Service } from 'cordis'
import { Dict } from 'cosmokit'
import { Entry as LoaderEntry } from '@cordisjs/loader'
import { Entry as ClientEntry } from '@cordisjs/plugin-webui'
import { PackageJson, SearchObject } from '@cordisjs/registry'
import {} from '@cordisjs/plugin-hmr'

declare module '@cordisjs/loader' {
namespace Entry {
interface Options {
label?: string | Dict<string>
collapse?: boolean
}
}
}

declare module '@cordisjs/plugin-webui' {
interface Events {
'manager.config.list'(): LoaderEntry.Options[]
'manager.config.create'(options: Omit<LoaderEntry.Options, 'id'> & EntryLocation): Promise<string>
'manager.config.update'(options: Omit<LoaderEntry.Options, 'id' | 'name'>): void
'manager.config.remove'(options: { id: string }): void
'manager.config.teleport'(options: { id: string } & EntryLocation): void
'manager.package.list'(): Promise<PackageProvider.Data[]>
'manager.package.runtime'(name: string): Promise<PackageProvider.RuntimeData>
'manager.service.list'(): Dict<string[]>
}
}

export interface Data {
config: LoaderEntry.Options[]
packages: Dict<PackageProvider.Data>
services: Dict<string[]>
}

interface EntryLocation {
parent?: string
position?: number
}

export default abstract class Manager extends Service {
static inject = ['loader']

entry?: ClientEntry
cache: Dict<PackageProvider.RuntimeData> = {}
debouncedRefresh: () => void

store = new WeakMap<Plugin, string>()

constructor(public ctx: Context) {
super(ctx, 'manager', true)

if (!ctx.loader?.writable) {
throw new Error('@cordisjs/plugin-config is only available for json/yaml config file')
}

this.debouncedRefresh = ctx.debounce(() => this.entry?.refresh(), 0)
this.installWebUI()
}

getConfig() {
return this.ctx.loader.config
}

getServices() {
const result = {} as Dict<string[]>
for (const [name, { type }] of Object.entries(this.ctx.root[Context.internal])) {
if (type !== 'service') continue
const instance = this.ctx.get(name)
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)
}
return result
}

installWebUI() {
this.ctx.inject(['webui'], (ctx) => {
this.entry = ctx.webui.addEntry({
dev: import.meta.resolve('../client/index.ts'),
prod: [
import.meta.resolve('../dist/index.js'),
import.meta.resolve('../dist/style.css'),
],
}, async () => ({
config: this.getConfig(),
packages: await this.getPackages(),
services: this.getServices(),
}))

ctx.on('config', () => this.entry?.refresh())
ctx.on('internal/service', () => this.entry?.refresh())

ctx.on('internal/runtime', scope => this.update(scope.runtime.plugin))
ctx.on('internal/fork', scope => this.update(scope.runtime.plugin))
ctx.on('internal/status', scope => this.update(scope.runtime.plugin))
ctx.on('hmr/reload', (reloads) => {
for (const [plugin] of reloads) {
this.update(plugin)
}
})

ctx.webui.addListener('manager.config.list', () => {
return this.getConfig()
})

ctx.webui.addListener('manager.config.create', (options) => {
const { parent, position, ...rest } = options
return ctx.loader.create(rest, parent, position)
})

ctx.webui.addListener('manager.config.update', (options) => {
throw new Error('Not implemented')
})

ctx.webui.addListener('manager.config.remove', (options) => {
return ctx.loader.remove(options.id)
})

ctx.webui.addListener('manager.config.teleport', (options) => {
throw new Error('Not implemented')
})

ctx.webui.addListener('manager.package.list', () => {
return this.getPackages()
})

ctx.webui.addListener('manager.package.runtime', async (name) => {
this.cache[name] = await this.parseExports(name)
this.entry?.refresh()
return this.cache[name]
})

ctx.webui.addListener('manager.service.list', () => {
return this.getServices()
})
})
}

abstract collect(forced: boolean): Promise<PackageProvider.Data[]>

async update(plugin: Plugin) {
const name = this.store.get(plugin)
if (!name || !this.cache[name]) return
this.cache[name] = await this.parseExports(name)
this.debouncedRefresh()
}

parseRuntime(state: MainScope, result: PackageProvider.RuntimeData) {
result.id = state.runtime.uid
result.forkable = state.runtime.isForkable
result.forks = Object.fromEntries(state.children
.filter(fork => fork.entry)
.map(fork => [fork.entry!.options.id, { status: fork.status }]))
}

async getPackages(forced = false) {
const objects = (await this.collect(forced)).slice()
for (const object of objects) {
object.name = object.package?.name || ''
if (!this.cache[object.name]) continue
object.runtime = this.cache[object.name]
}

return Object.fromEntries(objects.map(data => [data.name, data]))
}

async parseExports(name: string) {
try {
const exports = await this.ctx.loader.resolve(name)
if (exports) this.store.set(exports, name)
const result: PackageProvider.RuntimeData = { id: null }
result.schema = exports?.Config || exports?.schema
result.usage = exports?.usage
result.filter = exports?.filter
const inject = exports?.using || exports?.inject || []
if (Array.isArray(inject)) {
result.required = inject
result.optional = []
} else {
result.required = inject.required || []
result.optional = inject.optional || []
}

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

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

export namespace PackageProvider {
export interface Data extends Pick<SearchObject, 'shortname' | 'workspace' | 'manifest' | 'portable'> {
name?: string
runtime?: RuntimeData
package: Pick<PackageJson, 'name' | 'version' | 'peerDependencies' | 'peerDependenciesMeta'>
}

export interface RuntimeData {
id?: number | null
filter?: boolean
forkable?: boolean
schema?: Schema
usage?: string
required?: string[]
optional?: string[]
failed?: boolean
forks?: Dict<{
status?: ScopeStatus
}>
}
}
Loading

0 comments on commit c453740

Please sign in to comment.