mirror of
https://github.com/nocodb/nocodb.git
synced 2026-05-01 10:17:19 +00:00
339 lines
10 KiB
TypeScript
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,
|
|
}
|
|
})
|