Files
nocodb/packages/nc-gui/composables/usePlugin/index.ts

339 lines
10 KiB
TypeScript

import type { ExtensionManifest, PluginManifest, ScriptManifest } from './interface'
export enum PluginLib {
assets = 'assets',
modules = 'modules',
markdowns = 'markdownModules',
scripts = 'scripts',
}
export enum PluginType {
extension = 'extension',
script = 'script',
}
interface PluginTypeConfig {
basePath: string
supportedLibs: PluginLib[]
}
const PLUGIN_TYPE_CONFIGS: Record<PluginType, PluginTypeConfig> = {
[PluginType.extension]: {
basePath: '../../extensions/',
supportedLibs: [PluginLib.assets, PluginLib.modules, PluginLib.markdowns],
},
[PluginType.script]: {
basePath: '../../scripts/',
supportedLibs: [PluginLib.assets, PluginLib.modules, PluginLib.markdowns, PluginLib.scripts],
},
}
type PluginAssetStorage = {
[P in PluginType]: {
[L in PluginLib]?: Record<string, any>
}
}
export const usePlugin = createSharedComposable(() => {
const pluginGlobs = {
[PluginType.extension]: {
[PluginLib.assets]: import.meta.glob('../../extensions/*/assets/*', {
query: '?url',
import: 'default',
}),
[PluginLib.modules]: import.meta.glob('../../extensions/*/*.json', {
import: 'default',
}),
[PluginLib.markdowns]: import.meta.glob('../../extensions/*/*.md', {
query: '?raw',
import: 'default',
}),
},
[PluginType.script]: {
[PluginLib.assets]: import.meta.glob('../../scripts/*/assets/*', {
query: '?url',
import: 'default',
}),
[PluginLib.modules]: import.meta.glob('../../scripts/*/*.json', {
import: 'default',
}),
[PluginLib.markdowns]: import.meta.glob('../../scripts/*/*.md', {
query: '?raw',
import: 'default',
}),
[PluginLib.scripts]: import.meta.glob('../../scripts/*/index.txt', {
query: '?raw',
import: 'default',
}),
},
} as const
const pluginAssets: PluginAssetStorage = {
[PluginType.extension]: {
[PluginLib.assets]: {} as Record<string, string>,
[PluginLib.modules]: {} as Record<string, ExtensionManifest>,
[PluginLib.markdowns]: {} as Record<string, string>,
},
[PluginType.script]: {
[PluginLib.assets]: {} as Record<string, string>,
[PluginLib.modules]: {} as Record<string, ScriptManifest>,
[PluginLib.markdowns]: {} as Record<string, string>,
[PluginLib.scripts]: {} as Record<string, string>,
},
}
const pluginsLoaded = ref(false)
const availableExtensions = ref<ExtensionManifest[]>([])
const availableScripts = ref<ScriptManifest[]>([])
const { appInfo } = useGlobal()
const pluginCollections = {
[PluginType.extension]: {
available: availableExtensions,
disabledCount: 0,
},
[PluginType.script]: {
available: availableScripts,
disabledCount: 0,
},
}
const { isFeatureEnabled } = useBetaFeatureToggle()
const isPluginsEnabled = computed(() => isEeUI)
const isBetaPluginsEnabled = computed(() => isFeatureEnabled(FEATURE_FLAG.EXTENSIONS))
const availablePlugins = computed<PluginManifest[]>(() => [...availableExtensions.value, ...availableScripts.value])
const availableExtensionIds = computed(() => availableExtensions.value.map((e) => e.id))
const availableExtensionMapById = computed(() => {
return availableExtensions.value.reduce((acc, ext) => {
acc[ext.id] = ext
return acc
}, {} as Record<string, ExtensionManifest>)
})
const availableScriptIds = computed(() => availableScripts.value.map((s) => s.id))
const availableScriptMapById = computed(() => {
return availableScripts.value.reduce((acc, script) => {
acc[script.id] = script
return acc
}, {} as Record<string, ScriptManifest>)
})
const pluginDescriptionContent = ref<Record<string, string>>({})
const scriptContent = ref<Record<string, string>>({})
/**
* Get asset URL for a plugin
* @param pathOrUrl The asset path or URL
* @param type The plugin type (extension or script)
* @returns The asset URL
*/
const getPluginAssetUrl = (pathOrUrl: string, type: PluginType = PluginType.extension) => {
if (pathOrUrl.startsWith('http')) {
return pathOrUrl
} else {
const basePath = type === PluginType.extension ? '../../extensions/' : '../../scripts/'
const file = pluginAssets[type][PluginLib.assets]?.[`${basePath}${pathOrUrl}`]
return file || ''
}
}
/**
* Get script content for a script plugin
* @param scriptId The script ID
* @returns The script content
*/
const getScriptContent = (scriptId: string) => {
return scriptContent.value[scriptId] || ''
}
/**
* Process a plugin manifest
* @param path Path to the plugin manifest
* @param manifest The plugin manifest object
* @param type The plugin type
*/
const processPluginManifest = (path: string, manifest: PluginManifest, type: PluginType) => {
try {
// Set plugin type
manifest.type = type
// Initialize links array if not present
if (!Array.isArray(manifest.links)) {
manifest.links = []
}
// For extensions, ensure config.modalSize exists
if (type === PluginType.extension) {
const extManifest = manifest as ExtensionManifest
if (!extManifest?.config || !extManifest?.config?.modalSize) {
extManifest.config = {
...(extManifest.config || {}),
modalSize: 'lg',
}
}
}
if (
manifest?.disabled !== true &&
// Ensure the plugin is enabled for the current environment
(appInfo.value?.isOnPrem || (isEeUI && !manifest?.onPrem)) &&
(!manifest?.beta || isFeatureEnabled(FEATURE_FLAG.EXTENSIONS))
) {
// Add to available plugins collection
const existingPluginIndex = pluginCollections[type].available.value.findIndex((p) => p.id === manifest.id)
if (existingPluginIndex !== -1) {
pluginCollections[type].available.value.splice(existingPluginIndex, 1, manifest as any)
} else {
pluginCollections[type].available.value.push(manifest as any)
}
// Handle plugin description markdown
if (manifest.description && manifest.id) {
const basePath = type === PluginType.extension ? '../../extensions/' : '../../scripts/'
const markdownPath = `${basePath}${manifest.description}`
if (pluginAssets[type][PluginLib.markdowns]?.[markdownPath]) {
try {
const markdownContent = pluginAssets[type][PluginLib.markdowns]?.[markdownPath]
pluginDescriptionContent.value[manifest.id] = `${markdownContent}`
} catch (markdownError) {
console.error(`Failed to load Markdown file at ${markdownPath}:`, markdownError)
}
}
}
// For scripts, load the script content
if (type === PluginType.script && (manifest as ScriptManifest).entry && manifest.id) {
const scriptManifest = manifest as ScriptManifest
const scriptPath = `../../scripts/${scriptManifest.entry}`
if (pluginAssets[type][PluginLib.scripts]?.[scriptPath]) {
try {
const scriptFileContent = pluginAssets[type][PluginLib.scripts]?.[scriptPath]
scriptContent.value[manifest.id] = `${scriptFileContent}`
} catch (scriptError) {
console.error(`Failed to load script file at ${scriptPath}:`, scriptError)
}
}
}
} else {
// Increment disabled count
pluginCollections[type].disabledCount++
}
} catch (error) {
console.error(`Failed to process ${type} manifest at ${path}:`, error)
}
}
/**
* Load all plugins (extensions and scripts)
*/
const loadPlugins = async () => {
try {
// Reset counters
for (const collection of Object.values(pluginCollections)) {
collection.disabledCount = 0
}
// Step 1: Load assets for all plugin types
for (const pluginType of Object.keys(pluginGlobs) as PluginType[]) {
for (const [libKey, glob] of Object.entries(pluginGlobs[pluginType])) {
if (!glob) continue
for (const path of Object.keys(glob)) {
if (!glob[path]) continue
try {
const lib = libKey as PluginLib
if (pluginAssets[pluginType][lib]) {
pluginAssets[pluginType][lib]![path] = await glob[path]()
}
} catch (error) {
console.error(`Failed to load ${pluginType} file at ${path} for ${libKey}:`, error)
}
}
}
}
for (const type of Object.keys(PLUGIN_TYPE_CONFIGS) as PluginType[]) {
const modulesAssets = pluginAssets[type][PluginLib.modules]
if (!modulesAssets) continue
for (const [path, manifest] of Object.entries(modulesAssets)) {
processPluginManifest(path, manifest, type)
}
pluginCollections[type].available.value.sort((a, b) => (a.order ?? Infinity) - (b.order ?? Infinity))
}
pluginsLoaded.value = true
} catch (error) {
console.error('Error loading plugins:', error)
}
}
watch(
[() => isBetaPluginsEnabled.value, () => isPluginsEnabled.value, () => appInfo.value?.isOnPrem],
async () => {
availableExtensions.value = []
availableScripts.value = []
await loadPlugins()
},
{
immediate: true,
},
)
/**
* Find a plugin by ID regardless of type
* @param id The plugin ID
* @returns The plugin manifest or undefined if not found
*/
const findPluginById = (id: string): PluginManifest | undefined => {
const extension = availableExtensions.value.find((e) => e.id === id)
if (extension) return extension
return availableScripts.value.find((s) => s.id === id)
}
/**
* Get plugin description content
* @param pluginId The plugin ID
* @returns The description content or empty string if not found
*/
const getPluginDescription = (pluginId: string): string => {
return pluginDescriptionContent.value[pluginId] || ''
}
return {
// State
pluginsLoaded,
availableExtensions,
availableScripts,
availablePlugins,
availableExtensionIds,
availableExtensionMapById,
availableScriptIds,
availableScriptMapById,
isPluginsEnabled,
// Content getters
getPluginAssetUrl,
getScriptContent,
getPluginDescription,
pluginDescriptionContent,
// Utilities
findPluginById,
loadPlugins,
// Plugin types for external use
pluginTypes: PluginType,
}
})