Barazo AppView backend
barazo.forum
1import { join } from 'node:path'
2
3import type { Logger } from '../logger.js'
4
5import type { PluginContext, PluginHooks, LoadedPlugin } from './types.js'
6import type { PluginManifest } from '../../validation/plugin-manifest.js'
7
8// ---------------------------------------------------------------------------
9// Hook reference parsing
10// ---------------------------------------------------------------------------
11
12export function resolveHookRef(ref: string): { modulePath: string; exportName: string } | null {
13 const hashIndex = ref.indexOf('#')
14 if (hashIndex <= 0) return null
15 return {
16 modulePath: ref.slice(0, hashIndex),
17 exportName: ref.slice(hashIndex + 1),
18 }
19}
20
21// ---------------------------------------------------------------------------
22// Plugin short name
23// ---------------------------------------------------------------------------
24
25export function getPluginShortName(name: string): string {
26 if (name.startsWith('@barazo/plugin-')) return name.slice('@barazo/plugin-'.length)
27 if (name.startsWith('barazo-plugin-')) return name.slice('barazo-plugin-'.length)
28 return name
29}
30
31// ---------------------------------------------------------------------------
32// Hook execution
33// ---------------------------------------------------------------------------
34
35export async function executeHook(
36 hookName: string,
37 hookFn: (...args: unknown[]) => Promise<void> | void,
38 ctx: PluginContext,
39 logger: Logger,
40 pluginName: string,
41 ...extraArgs: unknown[]
42): Promise<boolean> {
43 try {
44 await hookFn(ctx, ...extraArgs)
45 logger.info({ plugin: pluginName, hook: hookName }, 'Plugin hook executed')
46 return true
47 } catch (err: unknown) {
48 logger.error({ err, plugin: pluginName, hook: hookName }, 'Plugin hook failed')
49 return false
50 }
51}
52
53// ---------------------------------------------------------------------------
54// Load hooks from manifest
55// ---------------------------------------------------------------------------
56
57const HOOK_NAMES = ['onInstall', 'onUninstall', 'onEnable', 'onDisable', 'onProfileSync'] as const
58
59export async function loadPluginHooks(
60 packagePath: string,
61 manifest: PluginManifest,
62 logger: Logger
63): Promise<PluginHooks> {
64 const hooks: PluginHooks = {}
65 const hookEntries = manifest.hooks
66 if (!hookEntries) return hooks
67
68 for (const name of HOOK_NAMES) {
69 const ref = hookEntries[name]
70 if (!ref) continue
71
72 const parsed = resolveHookRef(ref)
73 if (!parsed) {
74 logger.warn({ plugin: manifest.name, hook: name, ref }, 'Invalid hook reference, skipping')
75 continue
76 }
77
78 try {
79 const fullPath = join(packagePath, parsed.modulePath)
80 const mod = (await import(fullPath)) as Record<string, unknown>
81 const fn = mod[parsed.exportName]
82 if (typeof fn === 'function') {
83 // Each hook is assigned individually after type-checking the export.
84 ;(hooks as Record<string, unknown>)[name] = fn
85 } else {
86 logger.warn(
87 { plugin: manifest.name, hook: name, export: parsed.exportName },
88 'Hook export is not a function'
89 )
90 }
91 } catch (err: unknown) {
92 logger.error({ err, plugin: manifest.name, hook: name }, 'Failed to load hook module')
93 }
94 }
95
96 return hooks
97}
98
99// ---------------------------------------------------------------------------
100// Build LoadedPlugin from discovery result
101// ---------------------------------------------------------------------------
102
103export async function buildLoadedPlugin(
104 manifest: PluginManifest,
105 packagePath: string,
106 logger: Logger
107): Promise<LoadedPlugin> {
108 const hooks = await loadPluginHooks(packagePath, manifest, logger)
109
110 return {
111 name: manifest.name,
112 displayName: manifest.displayName,
113 version: manifest.version,
114 description: manifest.description,
115 source: manifest.source,
116 category: manifest.category,
117 manifest: manifest as unknown as Record<string, unknown>,
118 packagePath,
119 hooks,
120 ...(manifest.backend?.routes !== undefined && { routesPath: manifest.backend.routes }),
121 ...(manifest.backend?.migrations !== undefined && {
122 migrationsPath: manifest.backend.migrations,
123 }),
124 }
125}