mirror of
https://github.com/logseq/logseq.git
synced 2026-06-01 19:01:22 +00:00
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:
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user