LSPlugin.core.ts 23 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001
  1. import EventEmitter from 'eventemitter3'
  2. import {
  3. deepMerge,
  4. setupInjectedStyle,
  5. genID,
  6. setupInjectedTheme,
  7. setupInjectedUI,
  8. deferred,
  9. invokeHostExportedApi,
  10. isObject, withFileProtocol
  11. } from './helpers'
  12. import Debug from 'debug'
  13. import { LSPluginCaller, LSPMSG_READY, LSPMSG_SYNC, LSPMSG, LSPMSG_SETTINGS } from './LSPlugin.caller'
  14. import {
  15. ILSPluginThemeManager,
  16. LSPluginPkgConfig,
  17. StyleOptions,
  18. StyleString,
  19. ThemeOptions,
  20. UIOptions
  21. } from './LSPlugin'
  22. import { snakeCase } from 'snake-case'
  23. import DOMPurify from 'dompurify'
  24. import * as path from 'path'
  25. const debug = Debug('LSPlugin:core')
  26. declare global {
  27. interface Window {
  28. LSPluginCore: LSPluginCore
  29. }
  30. }
  31. type DeferredActor = ReturnType<typeof deferred>
  32. type LSPluginCoreOptions = {
  33. localUserConfigRoot: string
  34. }
  35. /**
  36. * User settings
  37. */
  38. class PluginSettings extends EventEmitter<'change'> {
  39. private _settings: Record<string, any> = {
  40. disabled: false
  41. }
  42. constructor (private _userPluginSettings: any) {
  43. super()
  44. Object.assign(this._settings, _userPluginSettings)
  45. }
  46. get<T = any> (k: string): T {
  47. return this._settings[k]
  48. }
  49. set (k: string | Record<string, any>, v?: any) {
  50. const o = deepMerge({}, this._settings)
  51. if (typeof k === 'string') {
  52. if (this._settings[k] == v) return
  53. this._settings[k] = v
  54. } else if (isObject(k)) {
  55. deepMerge(this._settings, k)
  56. } else {
  57. return
  58. }
  59. this.emit('change',
  60. Object.assign({}, this._settings), o)
  61. }
  62. toJSON () {
  63. return this._settings
  64. }
  65. }
  66. class PluginLogger extends EventEmitter<'change'> {
  67. private _logs: Array<[type: string, payload: any]> = []
  68. constructor (private _tag: string) {
  69. super()
  70. }
  71. write (type: string, payload: any[]) {
  72. let msg = payload.reduce((ac, it) => {
  73. if (it && it instanceof Error) {
  74. ac += `${it.message} ${it.stack}`
  75. } else {
  76. ac += it.toString()
  77. }
  78. return ac
  79. }, `[${this._tag}][${new Date().toLocaleTimeString()}] `)
  80. this._logs.push([type, msg])
  81. this.emit('change')
  82. }
  83. clear () {
  84. this._logs = []
  85. this.emit('change')
  86. }
  87. info (...args: any[]) {
  88. this.write('INFO', args)
  89. }
  90. error (...args: any[]) {
  91. this.write('ERROR', args)
  92. }
  93. warn (...args: any[]) {
  94. this.write('WARN', args)
  95. }
  96. toJSON () {
  97. return this._logs
  98. }
  99. }
  100. type UserPreferences = {
  101. theme: ThemeOptions
  102. externals: Array<string> // external plugin locations
  103. [key: string]: any
  104. }
  105. type PluginLocalOptions = {
  106. key?: string // Unique from Logseq Plugin Store
  107. entry: string // Plugin main file
  108. url: string // Plugin package fs location
  109. name: string
  110. version: string
  111. mode: 'shadow' | 'iframe'
  112. settings?: PluginSettings
  113. logger?: PluginLogger
  114. [key: string]: any
  115. }
  116. type PluginLocalUrl = Pick<PluginLocalOptions, 'url'> & { [key: string]: any }
  117. type RegisterPluginOpts = PluginLocalOptions | PluginLocalUrl
  118. type PluginLocalIdentity = string
  119. enum PluginLocalLoadStatus {
  120. LOADING = 'loading',
  121. UNLOADING = 'unloading',
  122. LOADED = 'loaded',
  123. UNLOADED = 'unload',
  124. ERROR = 'error'
  125. }
  126. function initUserSettingsHandlers (pluginLocal: PluginLocal) {
  127. const _ = (label: string): any => `settings:${label}`
  128. pluginLocal.on(_('update'), (attrs) => {
  129. if (!attrs) return
  130. pluginLocal.settings?.set(attrs)
  131. })
  132. }
  133. function initMainUIHandlers (pluginLocal: PluginLocal) {
  134. const _ = (label: string): any => `main-ui:${label}`
  135. pluginLocal.on(_('visible'), ({ visible, toggle }) => {
  136. const el = pluginLocal.getMainUI()
  137. el?.classList[toggle ? 'toggle' : (visible ? 'add' : 'remove')]('visible')
  138. // pluginLocal.caller!.callUserModel(LSPMSG, { type: _('visible'), payload: visible })
  139. // auto focus frame
  140. if (!pluginLocal.shadow && el) {
  141. (el as HTMLIFrameElement).contentWindow?.focus()
  142. }
  143. })
  144. pluginLocal.on(_('attrs'), (attrs: Record<string, any>) => {
  145. const el = pluginLocal.getMainUI()
  146. Object.entries(attrs).forEach(([k, v]) => {
  147. el?.setAttribute(k, v)
  148. })
  149. })
  150. pluginLocal.on(_('style'), (style: Record<string, any>) => {
  151. const el = pluginLocal.getMainUI()
  152. Object.entries(style).forEach(([k, v]) => {
  153. el!.style[k] = v
  154. })
  155. })
  156. }
  157. function initProviderHandlers (pluginLocal: PluginLocal) {
  158. let _ = (label: string): any => `provider:${label}`
  159. let themed = false
  160. pluginLocal.on(_('theme'), (theme: ThemeOptions) => {
  161. pluginLocal.themeMgr.registerTheme(
  162. pluginLocal.id,
  163. theme
  164. )
  165. if (!themed) {
  166. pluginLocal._dispose(() => {
  167. pluginLocal.themeMgr.unregisterTheme(pluginLocal.id)
  168. })
  169. themed = true
  170. }
  171. })
  172. pluginLocal.on(_('style'), (style: StyleString | StyleOptions) => {
  173. let key: string | undefined
  174. if (typeof style !== 'string') {
  175. key = style.key
  176. style = style.style
  177. }
  178. if (!style || !style.trim()) return
  179. pluginLocal._dispose(
  180. setupInjectedStyle(style, {
  181. 'data-injected-style': key ? `${key}-${pluginLocal.id}` : '',
  182. 'data-ref': pluginLocal.id
  183. })
  184. )
  185. })
  186. pluginLocal.on(_('ui'), (ui: UIOptions) => {
  187. pluginLocal._onHostMounted(() => {
  188. // safe template
  189. ui.template = DOMPurify.sanitize(ui.template)
  190. pluginLocal._dispose(
  191. setupInjectedUI.call(pluginLocal,
  192. ui, {
  193. 'data-ref': pluginLocal.id
  194. })
  195. )
  196. })
  197. })
  198. }
  199. function initApiProxyHandlers (pluginLocal: PluginLocal) {
  200. let _ = (label: string): any => `api:${label}`
  201. pluginLocal.on(_('call'), (payload) => {
  202. const rt = invokeHostExportedApi(payload.method, ...payload.args)
  203. const { _sync } = payload
  204. if (pluginLocal.shadow) {
  205. if (payload.actor) {
  206. payload.actor.resolve(rt)
  207. }
  208. return
  209. }
  210. if (_sync != null) {
  211. const reply = (result: any) => {
  212. pluginLocal.caller?.callUserModel(LSPMSG_SYNC, {
  213. result, _sync
  214. })
  215. }
  216. Promise.resolve(rt).then(reply, reply)
  217. }
  218. })
  219. }
  220. class IllegalPluginPackageError extends Error {
  221. constructor (message: string) {
  222. super(message)
  223. this.name = IllegalPluginPackageError.name
  224. }
  225. }
  226. class ExistedImportedPluginPackageError extends Error {
  227. constructor (message: string) {
  228. super(message)
  229. this.name = ExistedImportedPluginPackageError.name
  230. }
  231. }
  232. /**
  233. * Host plugin for local
  234. */
  235. class PluginLocal
  236. extends EventEmitter<'loaded' | 'unloaded' | 'beforeunload' | 'error'> {
  237. private _disposes: Array<() => Promise<any>> = []
  238. private _id: PluginLocalIdentity
  239. private _status: PluginLocalLoadStatus = PluginLocalLoadStatus.UNLOADED
  240. private _loadErr?: Error
  241. private _localRoot?: string
  242. private _userSettingsFile?: string
  243. private _caller?: LSPluginCaller
  244. /**
  245. * @param _options
  246. * @param _themeMgr
  247. * @param _ctx
  248. */
  249. constructor (
  250. private _options: PluginLocalOptions,
  251. private _themeMgr: ILSPluginThemeManager,
  252. private _ctx: LSPluginCore
  253. ) {
  254. super()
  255. this._id = _options.key || genID()
  256. initUserSettingsHandlers(this)
  257. initMainUIHandlers(this)
  258. initProviderHandlers(this)
  259. initApiProxyHandlers(this)
  260. }
  261. async _setupUserSettings () {
  262. const { _options } = this
  263. const key = _options.name.replace(/[^a-z0-9]/gi, '_').toLowerCase() + '_' + this.id
  264. const logger = _options.logger = new PluginLogger('Loader')
  265. try {
  266. const [userSettingsFilePath, userSettings] = await invokeHostExportedApi('load_plugin_user_settings', key)
  267. this._userSettingsFile = userSettingsFilePath
  268. const settings = _options.settings = new PluginSettings(userSettings)
  269. // observe settings
  270. settings.on('change', (a, b) => {
  271. debug('linked settings change', a)
  272. if (!a.disabled && b.disabled) {
  273. // Enable plugin
  274. this.load()
  275. }
  276. if (a.disabled && !b.disabled) {
  277. // Disable plugin
  278. this.unload()
  279. }
  280. if (a) {
  281. invokeHostExportedApi(`save_plugin_user_settings`, key, a)
  282. }
  283. })
  284. } catch (e) {
  285. debug('[load plugin user settings Error]', e)
  286. logger?.error(e)
  287. }
  288. }
  289. getMainUI (): HTMLElement | undefined {
  290. if (this.shadow) {
  291. return this.caller?._getSandboxShadowContainer()
  292. }
  293. return this.caller?._getSandboxIframeContainer()
  294. }
  295. async _preparePackageConfigs () {
  296. const { url } = this._options
  297. let pkg: any
  298. try {
  299. if (!url) {
  300. throw new Error('Can not resolve package config location')
  301. }
  302. debug('prepare package root', url)
  303. pkg = await invokeHostExportedApi('load_plugin_config', url)
  304. if (!pkg || (pkg = JSON.parse(pkg), !pkg)) {
  305. throw new Error(`Parse package config error #${url}/package.json`)
  306. }
  307. } catch (e) {
  308. throw new IllegalPluginPackageError(e.message)
  309. }
  310. // Pick legal attrs
  311. ['name', 'author', 'version', 'description'].forEach(k => {
  312. this._options[k] = pkg[k]
  313. })
  314. // TODO: How with local protocol
  315. const localRoot = this._localRoot = url
  316. const logseq: Partial<LSPluginPkgConfig> = pkg.logseq || {}
  317. const makeFullUrl = (loc, useFileProtocol = false) => {
  318. if (!loc) return
  319. const reg = /^(http|file|assets)/
  320. if (!reg.test(loc)) {
  321. const url = path.join(localRoot, loc)
  322. loc = reg.test(url) ? url : ('file://' + url)
  323. }
  324. return useFileProtocol ? loc : loc.replace('file:', 'assets:')
  325. }
  326. const validateMain = (main) => main && /\.(js|html)$/.test(main)
  327. // Entry from main
  328. if (validateMain(pkg.main)) {
  329. this._options.entry = makeFullUrl(pkg.main, true)
  330. if (logseq.mode) {
  331. this._options.mode = logseq.mode
  332. }
  333. }
  334. const icon = logseq.icon || pkg.icon
  335. if (icon) {
  336. this._options.icon = makeFullUrl(icon)
  337. }
  338. // TODO: strategy for Logseq plugins center
  339. if (logseq.id) {
  340. this._id = logseq.id
  341. } else {
  342. logseq.id = this.id
  343. try {
  344. await invokeHostExportedApi('save_plugin_config', url, { ...pkg, logseq })
  345. } catch (e) {
  346. debug('[save plugin ID Error] ', e)
  347. }
  348. }
  349. // Validate id
  350. const { registeredPlugins, isRegistering } = this._ctx
  351. if (isRegistering && registeredPlugins.has(logseq.id)) {
  352. throw new ExistedImportedPluginPackageError('prepare package Error')
  353. }
  354. return async () => {
  355. try {
  356. // 0. Install Themes
  357. let themes = logseq.themes
  358. if (themes) {
  359. await this._loadConfigThemes(
  360. Array.isArray(themes) ? themes : [themes]
  361. )
  362. }
  363. } catch (e) {
  364. debug('[prepare package effect Error]', e)
  365. }
  366. }
  367. }
  368. async _tryToNormalizeEntry () {
  369. let { entry, settings } = this.options
  370. let devEntry = settings?.get('_devEntry')
  371. if (devEntry) {
  372. this._options.entry = devEntry
  373. return
  374. }
  375. if (!entry.endsWith('.js')) return
  376. let sdkPath = await invokeHostExportedApi('_callApplication', 'getAppPath')
  377. let entryPath = await invokeHostExportedApi(
  378. 'write_user_tmp_file',
  379. `${this._id}_index.html`,
  380. `<!doctype html>
  381. <html lang="en">
  382. <head>
  383. <meta charset="UTF-8">
  384. <title>logseq plugin entry</title>
  385. <script src="${sdkPath}/js/lsplugin.user.js"></script>
  386. </head>
  387. <body>
  388. <div id="app"></div>
  389. <script src="${entry}"></script>
  390. </body>
  391. </html>`)
  392. this._options.entry = withFileProtocol(entryPath)
  393. }
  394. async _loadConfigThemes (themes: Array<ThemeOptions>) {
  395. themes.forEach((options) => {
  396. if (!options.url) return
  397. if (!options.url.startsWith('http') && this._localRoot) {
  398. options.url = path.join(this._localRoot, options.url)
  399. // file:// for native
  400. if (!options.url.startsWith('file:')) {
  401. options.url = 'assets://' + options.url
  402. }
  403. }
  404. // @ts-ignore
  405. this.emit('provider:theme', options)
  406. })
  407. }
  408. async load (readyIndicator?: DeferredActor) {
  409. if (this.pending) {
  410. return
  411. }
  412. this._status = PluginLocalLoadStatus.LOADING
  413. this._loadErr = undefined
  414. try {
  415. let installPackageThemes: () => Promise<void> = () => Promise.resolve()
  416. if (!this.options.entry) { // Themes package no entry field
  417. installPackageThemes = await this._preparePackageConfigs()
  418. }
  419. if (!this.settings) {
  420. await this._setupUserSettings()
  421. }
  422. if (!this.disabled) {
  423. await installPackageThemes.call(null)
  424. }
  425. if (this.disabled || !this.options.entry) {
  426. return
  427. }
  428. await this._tryToNormalizeEntry()
  429. this._caller = new LSPluginCaller(this)
  430. await this._caller.connectToChild()
  431. const readyFn = () => {
  432. this._caller?.callUserModel(LSPMSG_READY)
  433. }
  434. if (readyIndicator) {
  435. readyIndicator.promise.then(readyFn)
  436. } else {
  437. readyFn()
  438. }
  439. this._disposes.push(async () => {
  440. await this._caller?.destroy()
  441. })
  442. } catch (e) {
  443. debug('[Load Plugin Error] ', e)
  444. this.logger?.error(e)
  445. this._status = PluginLocalLoadStatus.ERROR
  446. this._loadErr = e
  447. } finally {
  448. if (!this._loadErr) {
  449. this._status = PluginLocalLoadStatus.LOADED
  450. }
  451. }
  452. }
  453. async reload () {
  454. debug('TODO: reload plugin', this.id)
  455. }
  456. /**
  457. * @param unregister If true delete plugin files
  458. */
  459. async unload (unregister: boolean = false) {
  460. if (this.pending) {
  461. return
  462. }
  463. if (unregister) {
  464. await this.unload()
  465. if (this.isInstalledInUserRoot) {
  466. debug('TODO: remove plugin local files from user home root :)')
  467. }
  468. return
  469. }
  470. try {
  471. this._status = PluginLocalLoadStatus.UNLOADING
  472. const eventBeforeUnload = {}
  473. // sync call
  474. try {
  475. this.emit('beforeunload', eventBeforeUnload)
  476. } catch (e) {
  477. console.error('[beforeunload Error]', e)
  478. }
  479. await this.dispose()
  480. this.emit('unloaded')
  481. } catch (e) {
  482. debug('[plugin unload Error]', e)
  483. } finally {
  484. this._status = PluginLocalLoadStatus.UNLOADED
  485. }
  486. }
  487. private async dispose () {
  488. for (const fn of this._disposes) {
  489. try {
  490. fn && (await fn())
  491. } catch (e) {
  492. console.error(this.debugTag, 'dispose Error', e)
  493. }
  494. }
  495. // clear
  496. this._disposes = []
  497. }
  498. _dispose (fn: any) {
  499. if (!fn) return
  500. this._disposes.push(fn)
  501. }
  502. _onHostMounted (callback: () => void) {
  503. const actor = this._ctx.hostMountedActor
  504. if (!actor || actor.settled) {
  505. callback()
  506. } else {
  507. actor?.promise.then(callback)
  508. }
  509. }
  510. get isInstalledInUserRoot () {
  511. const userRoot = this._ctx.options.localUserConfigRoot
  512. const plugRoot = this._localRoot
  513. return userRoot && plugRoot && plugRoot.startsWith(userRoot)
  514. }
  515. get loaded () {
  516. return this._status === PluginLocalLoadStatus.LOADED
  517. }
  518. get pending () {
  519. return [PluginLocalLoadStatus.LOADING, PluginLocalLoadStatus.UNLOADING]
  520. .includes(this._status)
  521. }
  522. get status (): PluginLocalLoadStatus {
  523. return this._status
  524. }
  525. get settings () {
  526. return this.options.settings
  527. }
  528. get logger () {
  529. return this.options.logger
  530. }
  531. get disabled () {
  532. return this.settings?.get('disabled')
  533. }
  534. get caller () {
  535. return this._caller
  536. }
  537. get id (): string {
  538. return this._id
  539. }
  540. get shadow (): boolean {
  541. return this.options.mode === 'shadow'
  542. }
  543. get options (): PluginLocalOptions {
  544. return this._options
  545. }
  546. get themeMgr (): ILSPluginThemeManager {
  547. return this._themeMgr
  548. }
  549. get debugTag () {
  550. return `[${this._options?.name} #${this._id}]`
  551. }
  552. get localRoot (): string {
  553. return this._localRoot || this._options.url
  554. }
  555. get loadErr (): Error | undefined {
  556. return this._loadErr
  557. }
  558. get userSettingsFile (): string | undefined {
  559. return this._userSettingsFile
  560. }
  561. toJSON () {
  562. const json = { ...this.options } as any
  563. json.id = this.id
  564. json.err = this.loadErr
  565. json.usf = this.userSettingsFile
  566. return json
  567. }
  568. }
  569. /**
  570. * Host plugin core
  571. */
  572. class LSPluginCore
  573. extends EventEmitter<'beforeenable' | 'enabled' | 'beforedisable' | 'disabled' | 'registered' | 'error' | 'unregistered' |
  574. 'theme-changed' | 'theme-selected' | 'settings-changed'>
  575. implements ILSPluginThemeManager {
  576. private _isRegistering = false
  577. private _readyIndicator?: DeferredActor
  578. private _hostMountedActor: DeferredActor = deferred()
  579. private _userPreferences: Partial<UserPreferences> = {}
  580. private _registeredThemes = new Map<PluginLocalIdentity, Array<ThemeOptions>>()
  581. private _registeredPlugins = new Map<PluginLocalIdentity, PluginLocal>()
  582. /**
  583. * @param _options
  584. */
  585. constructor (private _options: Partial<LSPluginCoreOptions>) {
  586. super()
  587. }
  588. async loadUserPreferences () {
  589. try {
  590. const settings = await invokeHostExportedApi(`load_user_preferences`)
  591. if (settings) {
  592. Object.assign(this._userPreferences, settings)
  593. }
  594. } catch (e) {
  595. debug('[load user preferences Error]', e)
  596. }
  597. }
  598. async saveUserPreferences (settings: Partial<UserPreferences>) {
  599. try {
  600. if (settings) {
  601. Object.assign(this._userPreferences, settings)
  602. }
  603. await invokeHostExportedApi(`save_user_preferences`, this._userPreferences)
  604. } catch (e) {
  605. debug('[save user preferences Error]', e)
  606. }
  607. }
  608. async activateUserPreferences () {
  609. const { theme } = this._userPreferences
  610. // 0. theme
  611. if (theme) {
  612. await this.selectTheme(theme, false)
  613. }
  614. }
  615. /**
  616. * @param plugins
  617. * @param initial
  618. */
  619. async register (
  620. plugins: Array<RegisterPluginOpts> | RegisterPluginOpts,
  621. initial = false
  622. ) {
  623. if (!Array.isArray(plugins)) {
  624. await this.register([plugins])
  625. return
  626. }
  627. try {
  628. this._isRegistering = true
  629. const userConfigRoot = this._options.localUserConfigRoot
  630. const readyIndicator = this._readyIndicator = deferred()
  631. await this.loadUserPreferences()
  632. const externals = new Set(this._userPreferences.externals || [])
  633. if (initial) {
  634. plugins = plugins.concat([...externals].filter(url => {
  635. return !plugins.length || (plugins as RegisterPluginOpts[]).every((p) => !p.entry && (p.url !== url))
  636. }).map(url => ({ url })))
  637. }
  638. for (const pluginOptions of plugins) {
  639. const { url } = pluginOptions as PluginLocalOptions
  640. const pluginLocal = new PluginLocal(pluginOptions as PluginLocalOptions, this, this)
  641. const timeLabel = `Load plugin #${pluginLocal.id}`
  642. console.time(timeLabel)
  643. await pluginLocal.load(readyIndicator)
  644. const { loadErr } = pluginLocal
  645. if (loadErr) {
  646. debug(`Failed load plugin #`, pluginOptions)
  647. this.emit('error', loadErr)
  648. if (
  649. loadErr instanceof IllegalPluginPackageError ||
  650. loadErr instanceof ExistedImportedPluginPackageError) {
  651. // TODO: notify global log system?
  652. continue
  653. }
  654. }
  655. console.timeEnd(timeLabel)
  656. pluginLocal.settings?.on('change', (a) => {
  657. this.emit('settings-changed', pluginLocal.id, a)
  658. pluginLocal.caller?.callUserModel(LSPMSG_SETTINGS, { payload: a })
  659. })
  660. this._registeredPlugins.set(pluginLocal.id, pluginLocal)
  661. this.emit('registered', pluginLocal)
  662. // external plugins
  663. if (!pluginLocal.isInstalledInUserRoot) {
  664. externals.add(url)
  665. }
  666. }
  667. await this.saveUserPreferences({ externals: Array.from(externals) })
  668. await this.activateUserPreferences()
  669. readyIndicator.resolve('ready')
  670. } catch (e) {
  671. console.error(e)
  672. } finally {
  673. this._isRegistering = false
  674. }
  675. }
  676. async reload (plugins: Array<PluginLocalIdentity> | PluginLocalIdentity) {
  677. if (!Array.isArray(plugins)) {
  678. await this.reload([plugins])
  679. return
  680. }
  681. for (const identity of plugins) {
  682. const p = this.ensurePlugin(identity)
  683. await p.reload()
  684. }
  685. }
  686. async unregister (plugins: Array<PluginLocalIdentity> | PluginLocalIdentity) {
  687. if (!Array.isArray(plugins)) {
  688. await this.unregister([plugins])
  689. return
  690. }
  691. const unregisteredExternals: Array<string> = []
  692. for (const identity of plugins) {
  693. const p = this.ensurePlugin(identity)
  694. if (!p.isInstalledInUserRoot) {
  695. unregisteredExternals.push(p.options.url)
  696. }
  697. await p.unload(true)
  698. this._registeredPlugins.delete(identity)
  699. this.emit('unregistered', identity)
  700. }
  701. let externals = this._userPreferences.externals || []
  702. if (externals.length && unregisteredExternals.length) {
  703. await this.saveUserPreferences({
  704. externals: externals.filter((it) => {
  705. return !unregisteredExternals.includes(it)
  706. })
  707. })
  708. }
  709. }
  710. async enable (plugin: PluginLocalIdentity) {
  711. const p = this.ensurePlugin(plugin)
  712. if (p.pending) return
  713. this.emit('beforeenable')
  714. p.settings?.set('disabled', false)
  715. // this.emit('enabled', p)
  716. }
  717. async disable (plugin: PluginLocalIdentity) {
  718. const p = this.ensurePlugin(plugin)
  719. if (p.pending) return
  720. this.emit('beforedisable')
  721. p.settings?.set('disabled', true)
  722. // this.emit('disabled', p)
  723. }
  724. async _hook (ns: string, type: string, payload?: any, pid?: string) {
  725. for (const [_, p] of this._registeredPlugins) {
  726. if (!pid || pid === p.id) {
  727. p.caller?.callUserModel(LSPMSG, {
  728. ns, type: snakeCase(type), payload
  729. })
  730. }
  731. }
  732. }
  733. hookApp (type: string, payload?: any, pid?: string) {
  734. this._hook(`hook:app`, type, payload, pid)
  735. }
  736. hookEditor (type: string, payload?: any, pid?: string) {
  737. this._hook(`hook:editor`, type, payload, pid)
  738. }
  739. _execDirective (tag: string, ...params: any[]) {
  740. }
  741. ensurePlugin (plugin: PluginLocalIdentity | PluginLocal) {
  742. if (plugin instanceof PluginLocal) {
  743. return plugin
  744. }
  745. const p = this._registeredPlugins.get(plugin)
  746. if (!p) {
  747. throw new Error(`plugin #${plugin} not existed.`)
  748. }
  749. return p
  750. }
  751. hostMounted () {
  752. this._hostMountedActor.resolve()
  753. }
  754. get registeredPlugins (): Map<PluginLocalIdentity, PluginLocal> {
  755. return this._registeredPlugins
  756. }
  757. get options () {
  758. return this._options
  759. }
  760. get readyIndicator (): DeferredActor | undefined {
  761. return this._readyIndicator
  762. }
  763. get hostMountedActor (): DeferredActor {
  764. return this._hostMountedActor
  765. }
  766. get isRegistering (): boolean {
  767. return this._isRegistering
  768. }
  769. get themes (): Map<PluginLocalIdentity, Array<ThemeOptions>> {
  770. return this._registeredThemes
  771. }
  772. async registerTheme (id: PluginLocalIdentity, opt: ThemeOptions): Promise<void> {
  773. debug('registered Theme #', id, opt)
  774. if (!id) return
  775. let themes: Array<ThemeOptions> = this._registeredThemes.get(id)!
  776. if (!themes) {
  777. this._registeredThemes.set(id, themes = [])
  778. }
  779. themes.push(opt)
  780. this.emit('theme-changed', this.themes, { id, ...opt })
  781. }
  782. async selectTheme (opt?: ThemeOptions, effect = true): Promise<void> {
  783. setupInjectedTheme(opt?.url)
  784. this.emit('theme-selected', opt)
  785. effect && this.saveUserPreferences({ theme: opt })
  786. }
  787. async unregisterTheme (id: PluginLocalIdentity): Promise<void> {
  788. debug('unregistered Theme #', id)
  789. if (!this._registeredThemes.has(id)) return
  790. this._registeredThemes.delete(id)
  791. this.emit('theme-changed', this.themes, { id })
  792. }
  793. }
  794. function setupPluginCore (options: any) {
  795. const pluginCore = new LSPluginCore(options)
  796. debug('=== 🔗 Setup Logseq Plugin System 🔗 ===')
  797. window.LSPluginCore = pluginCore
  798. }
  799. export {
  800. PluginLocal,
  801. setupPluginCore
  802. }