159 lines
4.7 KiB
TypeScript
159 lines
4.7 KiB
TypeScript
|
|
import React from 'react'
|
||
|
|
import type { LucideIcon } from 'lucide-react'
|
||
|
|
import type { FileItem } from '@/lib/api'
|
||
|
|
import type { TOCItem } from '@/lib/utils'
|
||
|
|
import type { FrontendModuleConfig, InternalModuleConfig, ModuleEndpoints } from '@shared/types/module'
|
||
|
|
import type { ModuleDefinition } from '@shared/modules/types'
|
||
|
|
import { moduleApiRegistry } from './api/modules/ModuleApiRegistry'
|
||
|
|
import { createModuleApi, ModuleApiInstance } from './api/createModuleApi'
|
||
|
|
|
||
|
|
export type { FrontendModuleConfig, InternalModuleConfig }
|
||
|
|
|
||
|
|
export interface FrontendModule extends InternalModuleConfig {
|
||
|
|
match: (file: FileItem) => boolean
|
||
|
|
render: (file: FileItem, isActive: boolean, onTocUpdated?: (toc: TOCItem[]) => void) => React.ReactNode
|
||
|
|
}
|
||
|
|
|
||
|
|
const createFileItem = (name: string, path: string): FileItem => ({
|
||
|
|
name,
|
||
|
|
path,
|
||
|
|
type: 'file',
|
||
|
|
size: 0,
|
||
|
|
modified: new Date().toISOString(),
|
||
|
|
})
|
||
|
|
|
||
|
|
class ModuleRegistryImpl {
|
||
|
|
private modules = new Map<string, InternalModuleConfig>()
|
||
|
|
|
||
|
|
register(config: FrontendModuleConfig): void {
|
||
|
|
if (this.modules.has(config.id)) {
|
||
|
|
throw new Error(`Module with ID '${config.id}' is already registered`)
|
||
|
|
}
|
||
|
|
|
||
|
|
const tabId = `${config.id}-tab`
|
||
|
|
const fileItem = createFileItem(config.name, tabId)
|
||
|
|
|
||
|
|
const internalConfig: InternalModuleConfig = {
|
||
|
|
...config,
|
||
|
|
tabId,
|
||
|
|
fileItem,
|
||
|
|
}
|
||
|
|
|
||
|
|
this.modules.set(config.id, internalConfig)
|
||
|
|
|
||
|
|
console.log(`[ModuleRegistry] Registering module: ${config.id}, basePath: ${config.basePath}, hasEndpoints: ${!!config.endpoints}, endpoints:`, config.endpoints)
|
||
|
|
|
||
|
|
if (config.endpoints && config.basePath) {
|
||
|
|
const apiInstance = createModuleApi({
|
||
|
|
id: config.id,
|
||
|
|
name: config.name,
|
||
|
|
basePath: config.basePath,
|
||
|
|
order: config.order,
|
||
|
|
endpoints: config.endpoints,
|
||
|
|
})
|
||
|
|
moduleApiRegistry.register(apiInstance)
|
||
|
|
console.log(`[ModuleRegistry] API registered for: ${config.id}`)
|
||
|
|
} else {
|
||
|
|
console.warn(`[ModuleRegistry] Skipping API registration for ${config.id}: endpoints=${!!config.endpoints}, basePath=${config.basePath}`)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
get(id: string): InternalModuleConfig | undefined {
|
||
|
|
return this.modules.get(id)
|
||
|
|
}
|
||
|
|
|
||
|
|
getAll(): InternalModuleConfig[] {
|
||
|
|
return Array.from(this.modules.values())
|
||
|
|
.sort((a, b) => a.order - b.order)
|
||
|
|
}
|
||
|
|
|
||
|
|
has(id: string): boolean {
|
||
|
|
return this.modules.has(id)
|
||
|
|
}
|
||
|
|
|
||
|
|
getFileItem(id: string): FileItem | undefined {
|
||
|
|
return this.modules.get(id)?.fileItem
|
||
|
|
}
|
||
|
|
|
||
|
|
getTabId(id: string): string | undefined {
|
||
|
|
return this.modules.get(id)?.tabId
|
||
|
|
}
|
||
|
|
|
||
|
|
getDefault(): InternalModuleConfig | undefined {
|
||
|
|
const all = this.getAll()
|
||
|
|
return all.length > 0 ? all[0] : undefined
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
export const registry = new ModuleRegistryImpl()
|
||
|
|
|
||
|
|
export function defineModule<
|
||
|
|
TEndpoints extends ModuleEndpoints
|
||
|
|
>(config: FrontendModuleConfig<TEndpoints>): FrontendModuleConfig<TEndpoints> {
|
||
|
|
return config
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface FrontendModuleOptions<
|
||
|
|
TEndpoints extends ModuleEndpoints = ModuleEndpoints
|
||
|
|
> {
|
||
|
|
icon: LucideIcon
|
||
|
|
component: React.ComponentType
|
||
|
|
}
|
||
|
|
|
||
|
|
export function createFrontendModule<
|
||
|
|
TEndpoints extends ModuleEndpoints
|
||
|
|
>(
|
||
|
|
definition: ModuleDefinition<string, TEndpoints>,
|
||
|
|
options: FrontendModuleOptions<TEndpoints>
|
||
|
|
): FrontendModuleConfig<TEndpoints> {
|
||
|
|
return {
|
||
|
|
id: definition.id,
|
||
|
|
name: definition.name,
|
||
|
|
order: definition.order,
|
||
|
|
basePath: definition.basePath,
|
||
|
|
endpoints: definition.endpoints,
|
||
|
|
icon: options.icon,
|
||
|
|
component: options.component,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
export function getModuleLegacy(file: FileItem): InternalModuleConfig | undefined {
|
||
|
|
return registry.getAll().find(m => m.fileItem?.path === file.path)
|
||
|
|
}
|
||
|
|
|
||
|
|
export function matchModule(file: FileItem): InternalModuleConfig | undefined {
|
||
|
|
return getModuleLegacy(file)
|
||
|
|
}
|
||
|
|
|
||
|
|
export const getAllModules = (): InternalModuleConfig[] => {
|
||
|
|
return registry.getAll()
|
||
|
|
}
|
||
|
|
|
||
|
|
export const getFileItem = (moduleId: string): FileItem | undefined => {
|
||
|
|
return registry.getFileItem(moduleId)
|
||
|
|
}
|
||
|
|
|
||
|
|
export const getModuleFileItem = getFileItem
|
||
|
|
|
||
|
|
export const getModuleTabId = (moduleId: string): string | undefined => {
|
||
|
|
return registry.getTabId(moduleId)
|
||
|
|
}
|
||
|
|
|
||
|
|
export const getModule = (moduleId: string): InternalModuleConfig | undefined => {
|
||
|
|
return registry.get(moduleId)
|
||
|
|
}
|
||
|
|
|
||
|
|
export const getDefaultModule = (): InternalModuleConfig | undefined => {
|
||
|
|
return registry.getDefault()
|
||
|
|
}
|
||
|
|
|
||
|
|
export function getModuleApi<TEndpoints extends ModuleEndpoints>(
|
||
|
|
moduleId: string
|
||
|
|
): ModuleApiInstance<TEndpoints> | undefined {
|
||
|
|
const api = moduleApiRegistry.get<ModuleApiInstance<TEndpoints>>(moduleId)
|
||
|
|
if (!api) {
|
||
|
|
console.error(`[getModuleApi] Failed to get API for module: ${moduleId}. Available APIs:`, Array.from((moduleApiRegistry as any).apis?.keys?.() || []))
|
||
|
|
}
|
||
|
|
return api
|
||
|
|
}
|