refactor: plugin libs (#12395)

This pull request refactors the plugin library infrastructure and adds new experimental features for hosted/sidebar renderers. The main changes include:

Purpose: Refactor plugin communication library (Postmate) to support MessageChannel for improved performance, add support for hosted/sidebar renderers in plugins, add new debug APIs, and consolidate helper functions.

Changes:

Added MessageChannel support to Postmate for optimized plugin-host communication with backward compatibility
Introduced hosted renderer and sidebar renderer APIs for plugins to register custom UI components
Added new app APIs: get_current_route, export_debug_log_db, reset_debug_log_db
Refactored helper functions from helpers.ts to common.ts and updated all import paths
Extended block property APIs to include class properties with default values
Added comprehensive documentation for experiments APIs and plugin development
Added E2E test for plugin marketplace installation
Version bump from 0.2.12 to 0.3.1
This commit is contained in:
Charlie
2026-04-14 14:29:22 +08:00
committed by GitHub
parent 4461a03fd5
commit a95483655b
29 changed files with 1665 additions and 449 deletions

View File

@@ -21,8 +21,8 @@ import {
injectTheme,
cleanInjectedUI,
PluginLogger,
} from './helpers'
import * as pluginHelpers from './helpers'
} from './common'
import * as pluginHelpers from './common'
import DOMPurify from 'dompurify'
import Debug from 'debug'
import {
@@ -86,25 +86,44 @@ class PluginSettings extends EventEmitter<'change' | 'reset'> {
return this._settings[k]
}
set(k: string | Record<string, any>, v?: any) {
set(k: string, v?: any) {
const o = deepMerge({}, this._settings)
if (typeof k === 'string') {
if (this._settings[k] == v) return
this._settings[k] = v
} else if (isObject(k)) {
this._settings = deepMerge(this._settings, k)
} else {
if (this._settings[k] === v) {
return
}
this._settings = {
...this._settings,
[k]: v,
}
this.emit('change', { ...this._settings }, o)
}
patch(input: Record<string, any>) {
if (!isObject(input)) {
return
}
const o = deepMerge({}, this._settings)
this._settings = deepMerge(this._settings, input)
this.emit('change', { ...this._settings }, o)
}
replace(value: Record<string, any>) {
const o = deepMerge({}, this._settings)
this._settings = {
disabled: false,
...(value || {}),
}
this.emit('change', { ...this._settings }, o)
}
set settings(value: Record<string, any>) {
const o = deepMerge({}, this._settings)
this._settings = value || {}
this.emit('change', { ...this._settings }, o)
this.replace(value)
}
get settings(): Record<string, any> {
@@ -115,9 +134,7 @@ class PluginSettings extends EventEmitter<'change' | 'reset'> {
this._schema = schema
if (syncSettings) {
const _settings = this._settings
this._settings = mergeSettingsWithSchema(_settings, schema)
this.emit('change', this._settings, _settings)
this.replace(mergeSettingsWithSchema(this._settings, schema))
}
}
@@ -129,7 +146,7 @@ class PluginSettings extends EventEmitter<'change' | 'reset'> {
// TODO: generated by schema
}
this.settings = val
this.replace(val)
this.emit('reset', val, o)
}
@@ -178,6 +195,15 @@ type RegisterPluginOpts = PluginLocalOptions | PluginLocalUrl
type PluginLocalIdentity = string
interface MainUILayoutData {
width: number
height: number
left: number
top: number
vw: number
vh: number
}
enum PluginLocalLoadStatus {
LOADING = 'loading',
UNLOADING = 'unloading',
@@ -201,7 +227,7 @@ function initUserSettingsHandlers(pluginLocal: PluginLocal) {
// settings:update
pluginLocal.on(_('update'), (attrs) => {
if (!attrs) return
pluginLocal.settings?.set(attrs)
pluginLocal.settings?.patch(attrs)
})
// settings:visible:changed
@@ -361,7 +387,11 @@ function initApiProxyHandlers(pluginLocal: PluginLocal) {
if (pluginLocal.shadow) {
if (payload.actor) {
payload.actor.resolve(ret)
if (ret?.hasOwnProperty(LSPMSG_ERROR_TAG)) {
payload.actor.reject(ret[LSPMSG_ERROR_TAG])
} else {
payload.actor.resolve(ret)
}
}
return
}
@@ -392,9 +422,16 @@ function convertToLSPResource(fullUrl: string, dotPluginRoot: string) {
}
class IllegalPluginPackageError extends Error {
constructor(message: string) {
url?: string
packageJsonPath?: string
constructor(
message: string,
options: Partial<Pick<IllegalPluginPackageError, 'url' | 'packageJsonPath'>> = {}
) {
super(message)
this.name = 'IllegalPluginPackageError'
Object.assign(this, options)
}
}
@@ -412,7 +449,8 @@ class PluginLocal extends EventEmitter<
'loaded' | 'unloaded' | 'beforeunload' | 'error' | string
> {
private _sdk: Partial<PluginLocalSDKMetadata> = {}
private _disposes: Array<() => Promise<any>> = []
private _runtimeDisposes: Array<() => Promise<any>> = []
private _registrationDisposes: Array<() => Promise<any>> = []
private _id: PluginLocalIdentity
private _status: PluginLocalLoadStatus = PluginLocalLoadStatus.UNLOADED
private _loadErr?: Error
@@ -420,6 +458,7 @@ class PluginLocal extends EventEmitter<
private _dotSettingsFile?: string
private _caller?: LSPluginCaller
private _logger?: PluginLogger = new PluginLogger('PluginLocal')
private _disposeSettingsObserver?: () => void
/**
* @param _options
@@ -435,6 +474,11 @@ class PluginLocal extends EventEmitter<
this._id = _options.key || genID()
this._disposeRegistration(async () => {
this._disposeSettingsObserver?.()
this._disposeSettingsObserver = undefined
})
initUserSettingsHandlers(this)
initMainUIHandlers(this)
initProviderHandlers(this)
@@ -445,7 +489,7 @@ class PluginLocal extends EventEmitter<
const { _options } = this
const logger = (this._logger = new PluginLogger(`Loader:${this.debugTag}`))
if (_options.settings && !reload) {
if (_options.settings && !reload && this._disposeSettingsObserver) {
return
}
@@ -461,31 +505,16 @@ class PluginLocal extends EventEmitter<
settings = _options.settings = new PluginSettings(userSettings)
}
this._disposeSettingsObserver?.()
this._disposeSettingsObserver = undefined
if (reload) {
settings.settings = userSettings
return
settings.replace(userSettings)
}
const handler = async (a, b) => {
const handler = async (a) => {
debug('Settings changed', this.debugTag, a)
if (!a.disabled && b.disabled) {
// Enable plugin
const [, freshSettings] = await loadFreshSettings()
freshSettings.disabled = false
a = Object.assign(a, freshSettings)
settings.settings = a
await this.load()
}
if (a.disabled && !b.disabled) {
// Disable plugin
const [, freshSettings] = await loadFreshSettings()
freshSettings.disabled = true
a = Object.assign(a, freshSettings)
await this.unload()
}
if (a) {
invokeHostExportedApi('save_plugin_user_settings', this.id, a)
}
@@ -494,7 +523,14 @@ class PluginLocal extends EventEmitter<
// observe settings
settings.on('change', handler)
return () => {}
const disposeSettingsObserver = () => {
settings.off('change', handler)
if (this._disposeSettingsObserver === disposeSettingsObserver) {
this._disposeSettingsObserver = undefined
}
}
this._disposeSettingsObserver = disposeSettingsObserver
} catch (e) {
debug('[load plugin user settings Error]', e)
logger?.error(e)
@@ -519,7 +555,8 @@ class PluginLocal extends EventEmitter<
return `${this.installedFromUserWebUrl}/${filePath}`
}
return `https://pub-80f42b85b62c40219354a834fcf2bbfa.r2.dev/${path.join(localRoot, filePath)}`
return `https://pub-80f42b85b62c40219354a834fcf2bbfa.r2.dev/${path.join(
localRoot, filePath)}`
}
const reg = /^(http|file)/
@@ -537,20 +574,35 @@ class PluginLocal extends EventEmitter<
let pkg: any = webPkg
if (!pkg) {
try {
if (!url) {
throw new Error('Can not resolve package config location')
}
let packageConfigError: string | undefined
if (!url) {
packageConfigError = 'Can not resolve package config location'
} else {
debug('prepare package root', url)
pkg = await invokeHostExportedApi('load_plugin_config', url)
try {
pkg = await invokeHostExportedApi('load_plugin_config', url)
if (!pkg || ((pkg = JSON.parse(pkg)), !pkg)) {
throw new Error(`Parse package config error #${url}/package.json`)
if (!pkg) {
packageConfigError = `Parse package config error #${url}/package.json`
} else {
pkg = JSON.parse(pkg)
if (!pkg) {
packageConfigError = `Parse package config error #${url}/package.json`
}
}
} catch (e: any) {
packageConfigError = e?.message || String(e)
}
} catch (e) {
throw new IllegalPluginPackageError(e.message)
}
if (packageConfigError) {
throw new IllegalPluginPackageError(packageConfigError, {
url,
packageJsonPath: url ? path.join(url, 'package.json') : undefined,
})
}
}
@@ -572,7 +624,8 @@ class PluginLocal extends EventEmitter<
})
const { repo, version } = this._options
const localRoot = (this._localRoot = this.isWebPlugin ? `${repo || url}/${version}` : safetyPathNormalize(url))
const localRoot = (this._localRoot = this.isWebPlugin ? `${repo ||
url}/${version}` : safetyPathNormalize(url))
const logseq: Partial<LSPluginPkgConfig> = pkg.logseq || {}
// const validateEntry = (main) => main && /\.(js|html)$/.test(main)
@@ -669,9 +722,9 @@ class PluginLocal extends EventEmitter<
<meta charset="UTF-8">
<title>logseq plugin entry</title>
${
IS_DEV
? `<script src="${sdkPathRoot}/lsplugin.user.js?v=${tag}"></script>`
: `<script src="https://cdn.jsdelivr.net/npm/@logseq/libs/dist/lsplugin.user.min.js?v=${tag}"></script>`
this.isWebPlugin
? `<script src="https://cdn.jsdelivr.net/npm/@logseq/libs/dist/lsplugin.user.min.js?v=${tag}"></script>`
: `<script src="${sdkPathRoot}/lsplugin.user.js?v=${tag}"></script>`
}
</head>
@@ -719,17 +772,12 @@ class PluginLocal extends EventEmitter<
return layouts || {}
}
async _saveLayoutsData(data) {
async _saveLayoutsData(data: any) {
const key = this.id + '_layouts'
await invokeHostExportedApi('save_plugin_user_settings', key, data)
}
async _persistMainUILayoutData(e: {
width: number
height: number
left: number
top: number
}) {
async _persistMainUILayoutData(e: MainUILayoutData) {
const layouts = await this._loadLayoutsData()
layouts.$$0 = e
await this._saveLayoutsData(layouts)
@@ -852,11 +900,14 @@ class PluginLocal extends EventEmitter<
reload: boolean
}>
) {
if (this.pending) {
if (this.pending || this.loaded) {
return
}
this._status = PluginLocalLoadStatus.LOADING
this._transitionStatus(PluginLocalLoadStatus.LOADING, [
PluginLocalLoadStatus.UNLOADED,
PluginLocalLoadStatus.ERROR,
])
this._loadErr = undefined
try {
@@ -865,7 +916,7 @@ class PluginLocal extends EventEmitter<
const installPackageThemes = await this._preparePackageConfigs()
this._dispose(await this._setupUserSettings(opts?.reload))
await this._setupUserSettings(opts?.reload)
if (!this.disabled) {
await installPackageThemes.call(null)
@@ -902,16 +953,17 @@ class PluginLocal extends EventEmitter<
} catch (e) {
this.logger.error('load', e, true)
this.dispose().catch(null)
this.disposeRuntime().catch(null)
this._status = PluginLocalLoadStatus.ERROR
this._loadErr = e
} finally {
if (!this._loadErr) {
if (this.disabled) {
this._status = PluginLocalLoadStatus.UNLOADED
} else {
this._status = PluginLocalLoadStatus.LOADED
}
this._transitionStatus(
this.disabled
? PluginLocalLoadStatus.UNLOADED
: PluginLocalLoadStatus.LOADED,
[PluginLocalLoadStatus.LOADING]
)
}
}
}
@@ -922,7 +974,11 @@ class PluginLocal extends EventEmitter<
}
this._ctx.emit('beforereload', this)
await this.unload()
if (this.loaded) {
await this.unload()
}
await this.load({ reload: true })
this._ctx.emit('reloaded', this)
}
@@ -935,8 +991,14 @@ class PluginLocal extends EventEmitter<
return
}
if (!unregister && !this.loaded) {
this._status = PluginLocalLoadStatus.UNLOADED
return
}
if (unregister) {
await this.unload()
await this.disposeRegistration()
if (this.isWebPlugin || this.isInstalledInLocalDotRoot) {
this._ctx.emit('unlink-plugin', this.id)
@@ -949,7 +1011,9 @@ class PluginLocal extends EventEmitter<
const eventBeforeUnload = { unregister }
if (this.loaded) {
this._status = PluginLocalLoadStatus.UNLOADING
this._transitionStatus(PluginLocalLoadStatus.UNLOADING, [
PluginLocalLoadStatus.LOADED,
])
try {
await this._caller?.callUserModel(
@@ -961,7 +1025,7 @@ class PluginLocal extends EventEmitter<
this.logger.error('beforeunload', e)
}
await this.dispose()
await this.disposeRuntime()
}
this.emit('unloaded')
@@ -972,22 +1036,51 @@ class PluginLocal extends EventEmitter<
}
}
private async dispose() {
for (const fn of this._disposes) {
private async _runDisposers(disposers: Array<() => Promise<any>>) {
for (const fn of disposers) {
try {
fn && (await fn())
} catch (e) {
console.error(this.debugTag, 'dispose Error', e)
}
}
}
private async disposeRuntime() {
await this._runDisposers(this._runtimeDisposes)
// clear
this._disposes = []
this._runtimeDisposes = []
}
private async disposeRegistration() {
await this._runDisposers(this._registrationDisposes)
// clear
this._registrationDisposes = []
}
_dispose(fn: any) {
if (!fn) return
this._disposes.push(fn)
this._runtimeDisposes.push(fn)
}
_disposeRegistration(fn: any) {
if (!fn) return
this._registrationDisposes.push(fn)
}
private _transitionStatus(
next: PluginLocalLoadStatus,
from?: PluginLocalLoadStatus[]
) {
if (from && !from.includes(this._status)) {
throw new Error(
`Invalid plugin status transition: ${this._status} -> ${next}`
)
}
this._status = next
}
_onHostMounted(callback: () => void) {
@@ -1118,7 +1211,9 @@ class PluginLocal extends EventEmitter<
json.err = this.loadErr
json.usf = this.dotSettingsFile
json.iir = this.isInstalledInLocalDotRoot
json.webMode = this.isWebPlugin ? (this.installedFromUserWebUrl ? 'user' : 'github') : false
json.webMode = this.isWebPlugin ? (this.installedFromUserWebUrl
? 'user'
: 'github') : false
json.lsr = this._resolveResourceFullUrl('/')
if (settings === false) {
@@ -1278,12 +1373,11 @@ class LSPluginCore
// @ts-expect-error
window.__debugPluginsPerfInfo = debugPerfInfo
const readyIndicator = (this._readyIndicator = deferred())
try {
this._isRegistering = true
const _userConfigRoot = this._options.dotConfigRoot
const readyIndicator = (this._readyIndicator = deferred())
await this.loadUserPreferences()
let externals = new Set(this._userPreferences.externals)
@@ -1355,16 +1449,22 @@ class LSPluginCore
}
}
pluginLocal.settings?.on('change', (a) => {
const onSettingsChange = (a) => {
this.emit('settings-changed', pluginLocal.id, a)
pluginLocal.caller?.callUserModel(LSPMSG_SETTINGS, { payload: a })
}
pluginLocal.settings?.on('change', onSettingsChange)
pluginLocal._disposeRegistration(() => {
pluginLocal.settings?.off('change', onSettingsChange)
})
this._registeredPlugins.set(pluginLocal.id, pluginLocal)
this.emit('registered', pluginLocal)
// external plugins
if (!pluginLocal.isWebPlugin && !pluginLocal.isInstalledInLocalDotRoot) {
if (!pluginLocal.isWebPlugin &&
!pluginLocal.isInstalledInLocalDotRoot) {
externals.add(url)
}
}
@@ -1376,6 +1476,10 @@ class LSPluginCore
} catch (e) {
console.error(e)
} finally {
if (!readyIndicator.settled) {
readyIndicator.resolve('ready')
}
this._isRegistering = false
this.emit('ready', perfTable)
debugPerfInfo()
@@ -1432,18 +1536,26 @@ class LSPluginCore
async enable(plugin: PluginLocalIdentity) {
const p = this.ensurePlugin(plugin)
if (p.pending) return
if (!p.disabled && p.loaded) return
this.emit('beforeenable')
p.settings?.set('disabled', false)
await p.load()
this.emit('enabled', p.id)
}
async disable(plugin: PluginLocalIdentity) {
const p = this.ensurePlugin(plugin)
if (p.pending) return
if (p.disabled && !p.loaded) return
this.emit('beforedisable')
p.settings?.set('disabled', true)
await p.unload()
this.emit('disabled', p.id)
}