LSPlugin.core.ts 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302
  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. getSDKPathRoot,
  12. PROTOCOL_FILE, URL_LSP,
  13. safetyPathJoin,
  14. path, safetyPathNormalize
  15. } from './helpers'
  16. import * as pluginHelpers from './helpers'
  17. import Debug from 'debug'
  18. import {
  19. LSPluginCaller,
  20. LSPMSG_READY, LSPMSG_SYNC,
  21. LSPMSG, LSPMSG_SETTINGS,
  22. LSPMSG_ERROR_TAG, LSPMSG_BEFORE_UNLOAD, AWAIT_LSPMSGFn
  23. } from './LSPlugin.caller'
  24. import {
  25. ILSPluginThemeManager,
  26. LSPluginPkgConfig,
  27. StyleOptions,
  28. StyleString,
  29. ThemeOptions, UIContainerAttrs,
  30. UIOptions
  31. } from './LSPlugin'
  32. import { snakeCase } from 'snake-case'
  33. const debug = Debug('LSPlugin:core')
  34. const DIR_PLUGINS = 'plugins'
  35. declare global {
  36. interface Window {
  37. LSPluginCore: LSPluginCore
  38. }
  39. }
  40. type DeferredActor = ReturnType<typeof deferred>
  41. type LSPluginCoreOptions = {
  42. dotConfigRoot: string
  43. }
  44. /**
  45. * User settings
  46. */
  47. class PluginSettings extends EventEmitter<'change'> {
  48. private _settings: Record<string, any> = {
  49. disabled: false
  50. }
  51. constructor (private _userPluginSettings: any) {
  52. super()
  53. Object.assign(this._settings, _userPluginSettings)
  54. }
  55. get<T = any> (k: string): T {
  56. return this._settings[k]
  57. }
  58. set (k: string | Record<string, any>, v?: any) {
  59. const o = deepMerge({}, this._settings)
  60. if (typeof k === 'string') {
  61. if (this._settings[k] == v) return
  62. this._settings[k] = v
  63. } else if (isObject(k)) {
  64. deepMerge(this._settings, k)
  65. } else {
  66. return
  67. }
  68. this.emit('change',
  69. Object.assign({}, this._settings), o)
  70. }
  71. set settings (value: Record<string, any>) {
  72. this._settings = value
  73. }
  74. get settings (): Record<string, any> {
  75. return this._settings
  76. }
  77. toJSON () {
  78. return this._settings
  79. }
  80. }
  81. class PluginLogger extends EventEmitter<'change'> {
  82. private _logs: Array<[type: string, payload: any]> = []
  83. constructor (private _tag: string) {
  84. super()
  85. }
  86. write (type: string, payload: any[]) {
  87. let msg = payload.reduce((ac, it) => {
  88. if (it && it instanceof Error) {
  89. ac += `${it.message} ${it.stack}`
  90. } else {
  91. ac += it.toString()
  92. }
  93. return ac
  94. }, `[${this._tag}][${new Date().toLocaleTimeString()}] `)
  95. this._logs.push([type, msg])
  96. this.emit('change')
  97. }
  98. clear () {
  99. this._logs = []
  100. this.emit('change')
  101. }
  102. info (...args: any[]) {
  103. this.write('INFO', args)
  104. }
  105. error (...args: any[]) {
  106. this.write('ERROR', args)
  107. }
  108. warn (...args: any[]) {
  109. this.write('WARN', args)
  110. }
  111. toJSON () {
  112. return this._logs
  113. }
  114. }
  115. type UserPreferences = {
  116. theme: ThemeOptions
  117. externals: Array<string> // external plugin locations
  118. [key: string]: any
  119. }
  120. type PluginLocalOptions = {
  121. key?: string // Unique from Logseq Plugin Store
  122. entry: string // Plugin main file
  123. url: string // Plugin package absolute fs location
  124. name: string
  125. version: string
  126. mode: 'shadow' | 'iframe'
  127. settings?: PluginSettings
  128. logger?: PluginLogger
  129. effect?: boolean
  130. [key: string]: any
  131. }
  132. type PluginLocalUrl = Pick<PluginLocalOptions, 'url'> & { [key: string]: any }
  133. type RegisterPluginOpts = PluginLocalOptions | PluginLocalUrl
  134. type PluginLocalIdentity = string
  135. enum PluginLocalLoadStatus {
  136. LOADING = 'loading',
  137. UNLOADING = 'unloading',
  138. LOADED = 'loaded',
  139. UNLOADED = 'unload',
  140. ERROR = 'error'
  141. }
  142. function initUserSettingsHandlers (pluginLocal: PluginLocal) {
  143. const _ = (label: string): any => `settings:${label}`
  144. pluginLocal.on(_('update'), (attrs) => {
  145. if (!attrs) return
  146. pluginLocal.settings?.set(attrs)
  147. })
  148. }
  149. function initMainUIHandlers (pluginLocal: PluginLocal) {
  150. const _ = (label: string): any => `main-ui:${label}`
  151. pluginLocal.on(_('visible'), ({ visible, toggle, cursor, autoFocus }) => {
  152. const el = pluginLocal.getMainUIContainer()
  153. el?.classList[toggle ? 'toggle' : (visible ? 'add' : 'remove')]('visible')
  154. // pluginLocal.caller!.callUserModel(LSPMSG, { type: _('visible'), payload: visible })
  155. // auto focus frame
  156. if (visible) {
  157. if (!pluginLocal.shadow && el && (autoFocus !== false)) {
  158. (el.querySelector('iframe') as HTMLIFrameElement)?.contentWindow?.focus()
  159. }
  160. }
  161. if (cursor) {
  162. invokeHostExportedApi('restore_editing_cursor')
  163. }
  164. })
  165. pluginLocal.on(_('attrs'), (attrs: Partial<UIContainerAttrs>) => {
  166. const el = pluginLocal.getMainUIContainer()
  167. Object.entries(attrs).forEach(([k, v]) => {
  168. el?.setAttribute(k, v)
  169. if (k === 'draggable' && v) {
  170. pluginLocal._dispose(
  171. pluginLocal._setupDraggableContainer(el, {
  172. title: pluginLocal.options.name,
  173. close: () => {
  174. pluginLocal.caller.call('sys:ui:visible', { toggle: true })
  175. }
  176. }))
  177. }
  178. if (k === 'resizable' && v) {
  179. pluginLocal._dispose(
  180. pluginLocal._setupResizableContainer(el))
  181. }
  182. })
  183. })
  184. pluginLocal.on(_('style'), (style: Record<string, any>) => {
  185. const el = pluginLocal.getMainUIContainer()
  186. const isInitedLayout = !!el.dataset.inited_layout
  187. Object.entries(style).forEach(([k, v]) => {
  188. if (isInitedLayout && [
  189. 'left', 'top', 'bottom', 'right', 'width', 'height'
  190. ].includes(k)) {
  191. return
  192. }
  193. el!.style[k] = v
  194. })
  195. })
  196. }
  197. function initProviderHandlers (pluginLocal: PluginLocal) {
  198. let _ = (label: string): any => `provider:${label}`
  199. let themed = false
  200. pluginLocal.on(_('theme'), (theme: ThemeOptions) => {
  201. pluginLocal.themeMgr.registerTheme(
  202. pluginLocal.id,
  203. theme
  204. )
  205. if (!themed) {
  206. pluginLocal._dispose(() => {
  207. pluginLocal.themeMgr.unregisterTheme(pluginLocal.id)
  208. })
  209. themed = true
  210. }
  211. })
  212. pluginLocal.on(_('style'), (style: StyleString | StyleOptions) => {
  213. let key: string | undefined
  214. if (typeof style !== 'string') {
  215. key = style.key
  216. style = style.style
  217. }
  218. if (!style || !style.trim()) return
  219. pluginLocal._dispose(
  220. setupInjectedStyle(style, {
  221. 'data-injected-style': key ? `${key}-${pluginLocal.id}` : '',
  222. 'data-ref': pluginLocal.id
  223. })
  224. )
  225. })
  226. pluginLocal.on(_('ui'), (ui: UIOptions) => {
  227. pluginLocal._onHostMounted(() => {
  228. pluginLocal._dispose(
  229. setupInjectedUI.call(pluginLocal,
  230. ui, Object.assign({
  231. 'data-ref': pluginLocal.id
  232. }, ui.attrs || {}),
  233. ({ el, float }) => {
  234. if (!float) return
  235. const identity = el.dataset.identity
  236. pluginLocal.layoutCore.move_container_to_top(identity)
  237. }))
  238. })
  239. })
  240. }
  241. function initApiProxyHandlers (pluginLocal: PluginLocal) {
  242. let _ = (label: string): any => `api:${label}`
  243. pluginLocal.on(_('call'), async (payload) => {
  244. let ret: any
  245. try {
  246. ret = await invokeHostExportedApi(payload.method, ...payload.args)
  247. } catch (e) {
  248. ret = {
  249. [LSPMSG_ERROR_TAG]: e,
  250. }
  251. }
  252. const { _sync } = payload
  253. if (pluginLocal.shadow) {
  254. if (payload.actor) {
  255. payload.actor.resolve(ret)
  256. }
  257. return
  258. }
  259. if (_sync != null) {
  260. const reply = (result: any) => {
  261. pluginLocal.caller?.callUserModel(LSPMSG_SYNC, {
  262. result, _sync
  263. })
  264. }
  265. Promise.resolve(ret).then(reply, reply)
  266. }
  267. })
  268. }
  269. function convertToLSPResource (fullUrl: string, dotPluginRoot: string) {
  270. if (
  271. dotPluginRoot &&
  272. fullUrl.startsWith(PROTOCOL_FILE + dotPluginRoot)
  273. ) {
  274. fullUrl = safetyPathJoin(
  275. URL_LSP, fullUrl.substr(PROTOCOL_FILE.length + dotPluginRoot.length))
  276. }
  277. return fullUrl
  278. }
  279. class IllegalPluginPackageError extends Error {
  280. constructor (message: string) {
  281. super(message)
  282. this.name = IllegalPluginPackageError.name
  283. }
  284. }
  285. class ExistedImportedPluginPackageError extends Error {
  286. constructor (message: string) {
  287. super(message)
  288. this.name = ExistedImportedPluginPackageError.name
  289. }
  290. }
  291. /**
  292. * Host plugin for local
  293. */
  294. class PluginLocal
  295. extends EventEmitter<'loaded' | 'unloaded' | 'beforeunload' | 'error'> {
  296. private _disposes: Array<() => Promise<any>> = []
  297. private _id: PluginLocalIdentity
  298. private _status: PluginLocalLoadStatus = PluginLocalLoadStatus.UNLOADED
  299. private _loadErr?: Error
  300. private _localRoot?: string
  301. private _dotSettingsFile?: string
  302. private _caller?: LSPluginCaller
  303. /**
  304. * @param _options
  305. * @param _themeMgr
  306. * @param _ctx
  307. */
  308. constructor (
  309. private _options: PluginLocalOptions,
  310. private _themeMgr: ILSPluginThemeManager,
  311. private _ctx: LSPluginCore
  312. ) {
  313. super()
  314. this._id = _options.key || genID()
  315. initUserSettingsHandlers(this)
  316. initMainUIHandlers(this)
  317. initProviderHandlers(this)
  318. initApiProxyHandlers(this)
  319. }
  320. async _setupUserSettings (
  321. reload?: boolean
  322. ) {
  323. const { _options } = this
  324. const logger = _options.logger = new PluginLogger('Loader')
  325. if (_options.settings && !reload) {
  326. return
  327. }
  328. try {
  329. const loadFreshSettings = () => invokeHostExportedApi('load_plugin_user_settings', this.id)
  330. const [userSettingsFilePath, userSettings] = await loadFreshSettings()
  331. this._dotSettingsFile = userSettingsFilePath
  332. let settings = _options.settings
  333. if (!settings) {
  334. settings = _options.settings = new PluginSettings(userSettings)
  335. }
  336. if (reload) {
  337. settings.settings = userSettings
  338. return
  339. }
  340. const handler = async (a, b) => {
  341. debug('Settings changed', this.debugTag, a)
  342. if (!a.disabled && b.disabled) {
  343. // Enable plugin
  344. const [, freshSettings] = await loadFreshSettings()
  345. freshSettings.disabled = false
  346. a = deepMerge(a, freshSettings)
  347. settings.settings = a
  348. await this.load()
  349. }
  350. if (a.disabled && !b.disabled) {
  351. // Disable plugin
  352. const [, freshSettings] = await loadFreshSettings()
  353. freshSettings.disabled = true
  354. a = deepMerge(a, freshSettings)
  355. await this.unload()
  356. }
  357. if (a) {
  358. invokeHostExportedApi(`save_plugin_user_settings`, this.id, a)
  359. }
  360. }
  361. // observe settings
  362. settings.on('change', handler)
  363. return () => {}
  364. } catch (e) {
  365. debug('[load plugin user settings Error]', e)
  366. logger?.error(e)
  367. }
  368. }
  369. getMainUIContainer (): HTMLElement | undefined {
  370. if (this.shadow) {
  371. return this.caller?._getSandboxShadowContainer()
  372. }
  373. return this.caller?._getSandboxIframeContainer()
  374. }
  375. _resolveResourceFullUrl (filePath: string, localRoot?: string) {
  376. if (!filePath?.trim()) return
  377. localRoot = localRoot || this._localRoot
  378. const reg = /^(http|file)/
  379. if (!reg.test(filePath)) {
  380. const url = path.join(localRoot, filePath)
  381. filePath = reg.test(url) ? url : (PROTOCOL_FILE + url)
  382. }
  383. return (!this.options.effect && this.isInstalledInDotRoot) ?
  384. convertToLSPResource(filePath, this.dotPluginsRoot) : filePath
  385. }
  386. async _preparePackageConfigs () {
  387. const { url } = this._options
  388. let pkg: any
  389. try {
  390. if (!url) {
  391. throw new Error('Can not resolve package config location')
  392. }
  393. debug('prepare package root', url)
  394. pkg = await invokeHostExportedApi('load_plugin_config', url)
  395. if (!pkg || (pkg = JSON.parse(pkg), !pkg)) {
  396. throw new Error(`Parse package config error #${url}/package.json`)
  397. }
  398. } catch (e) {
  399. throw new IllegalPluginPackageError(e.message)
  400. }
  401. const localRoot = this._localRoot = safetyPathNormalize(url)
  402. const logseq: Partial<LSPluginPkgConfig> = pkg.logseq || {}
  403. // Pick legal attrs
  404. ;['name', 'author', 'repository', 'version',
  405. 'description', 'repo', 'title', 'effect',
  406. ].concat(!this.isInstalledInDotRoot ? ['devEntry'] : []).forEach(k => {
  407. this._options[k] = pkg[k]
  408. })
  409. const validateEntry = (main) => main && /\.(js|html)$/.test(main)
  410. // Entry from main
  411. const entry = logseq.entry || logseq.main || pkg.main
  412. if (validateEntry(entry)) { // Theme has no main
  413. this._options.entry = this._resolveResourceFullUrl(entry, localRoot)
  414. this._options.devEntry = logseq.devEntry
  415. if (logseq.mode) {
  416. this._options.mode = logseq.mode
  417. }
  418. }
  419. const title = logseq.title || pkg.title
  420. const icon = logseq.icon || pkg.icon
  421. this._options.title = title
  422. this._options.icon = icon &&
  423. this._resolveResourceFullUrl(icon)
  424. // TODO: strategy for Logseq plugins center
  425. if (this.isInstalledInDotRoot) {
  426. this._id = path.basename(localRoot)
  427. } else {
  428. if (logseq.id) {
  429. this._id = logseq.id
  430. } else {
  431. logseq.id = this.id
  432. try {
  433. await invokeHostExportedApi('save_plugin_config', url, { ...pkg, logseq })
  434. } catch (e) {
  435. debug('[save plugin ID Error] ', e)
  436. }
  437. }
  438. }
  439. // Validate id
  440. const { registeredPlugins, isRegistering } = this._ctx
  441. if (isRegistering && registeredPlugins.has(logseq.id)) {
  442. throw new ExistedImportedPluginPackageError('prepare package Error')
  443. }
  444. return async () => {
  445. try {
  446. // 0. Install Themes
  447. let themes = logseq.themes
  448. if (themes) {
  449. await this._loadConfigThemes(
  450. Array.isArray(themes) ? themes : [themes]
  451. )
  452. }
  453. } catch (e) {
  454. debug('[prepare package effect Error]', e)
  455. }
  456. }
  457. }
  458. async _tryToNormalizeEntry () {
  459. let { entry, settings, devEntry } = this.options
  460. devEntry = devEntry || settings?.get('_devEntry')
  461. if (devEntry) {
  462. this._options.entry = devEntry
  463. return
  464. }
  465. if (!entry.endsWith('.js')) return
  466. let dirPathInstalled = null
  467. let tmp_file_method = 'write_user_tmp_file'
  468. if (this.isInstalledInDotRoot) {
  469. tmp_file_method = 'write_dotdir_file'
  470. dirPathInstalled = this._localRoot.replace(this.dotPluginsRoot, '')
  471. dirPathInstalled = path.join(DIR_PLUGINS, dirPathInstalled)
  472. }
  473. let sdkPathRoot = await getSDKPathRoot()
  474. let entryPath = await invokeHostExportedApi(
  475. tmp_file_method,
  476. `${this._id}_index.html`,
  477. `<!doctype html>
  478. <html lang="en">
  479. <head>
  480. <meta charset="UTF-8">
  481. <title>logseq plugin entry</title>
  482. <script src="${sdkPathRoot}/lsplugin.user.js"></script>
  483. </head>
  484. <body>
  485. <div id="app"></div>
  486. <script src="${entry}"></script>
  487. </body>
  488. </html>`, dirPathInstalled)
  489. entry = convertToLSPResource(
  490. withFileProtocol(path.normalize(entryPath)),
  491. this.dotPluginsRoot
  492. )
  493. this._options.entry = entry
  494. }
  495. async _loadConfigThemes (themes: Array<ThemeOptions>) {
  496. themes.forEach((options) => {
  497. if (!options.url) return
  498. if (!options.url.startsWith('http') && this._localRoot) {
  499. options.url = path.join(this._localRoot, options.url)
  500. // file:// for native
  501. if (!options.url.startsWith('file:')) {
  502. options.url = 'assets://' + options.url
  503. }
  504. }
  505. // @ts-ignore
  506. this.emit('provider:theme', options)
  507. })
  508. }
  509. _persistMainUILayoutData (e: { width: number, height: number, left: number, top: number }) {
  510. const layouts = this.settings.get('layouts') || []
  511. layouts[0] = e
  512. this.settings.set('layout', layouts)
  513. }
  514. _setupDraggableContainer (
  515. el: HTMLElement,
  516. opts: Partial<{ key: string, title: string, close: () => void }> = {}): () => void {
  517. const ds = el.dataset
  518. if (ds.inited_draggable) return
  519. if (!ds.identity) {
  520. ds.identity = 'dd-' + genID()
  521. }
  522. const isInjectedUI = !!opts.key
  523. const handle = document.createElement('div')
  524. handle.classList.add('draggable-handle')
  525. handle.innerHTML = `
  526. <div class="th">
  527. <div class="l"><h3>${opts.title || ''}</h3></div>
  528. <div class="r">
  529. <a class="button x"><i class="ti ti-x"></i></a>
  530. </div>
  531. </div>
  532. `
  533. handle.querySelector('.x')
  534. .addEventListener('click', (e) => {
  535. opts?.close?.()
  536. e.stopPropagation()
  537. }, false)
  538. handle.addEventListener('mousedown', (e) => {
  539. const target = e.target as HTMLElement
  540. if (target?.closest('.r')) {
  541. e.stopPropagation()
  542. e.preventDefault()
  543. return
  544. }
  545. }, false)
  546. el.prepend(handle)
  547. // move to top
  548. el.addEventListener('mousedown', (e) => {
  549. this.layoutCore.move_container_to_top(ds.identity)
  550. }, true)
  551. const setTitle = (title) => {
  552. handle.querySelector('h3').textContent = title
  553. }
  554. const dispose = this.layoutCore.setup_draggable_container_BANG_(el,
  555. !isInjectedUI ? this._persistMainUILayoutData.bind(this) : () => {})
  556. ds.inited_draggable = 'true'
  557. if (opts.title) {
  558. setTitle(opts.title)
  559. }
  560. // click outside
  561. let removeOutsideListener = null
  562. if (ds.close === 'outside') {
  563. const handler = (e) => {
  564. const target = e.target
  565. if (!el.contains(target)) {
  566. opts.close()
  567. }
  568. }
  569. document.addEventListener('click', handler, false)
  570. removeOutsideListener = () => {
  571. document.removeEventListener('click', handler)
  572. }
  573. }
  574. return () => {
  575. dispose()
  576. removeOutsideListener?.()
  577. }
  578. }
  579. _setupResizableContainer (el: HTMLElement, key?: string): () => void {
  580. const ds = el.dataset
  581. if (ds.inited_resizable) return
  582. if (!ds.identity) {
  583. ds.identity = 'dd-' + genID()
  584. }
  585. const handle = document.createElement('div')
  586. handle.classList.add('resizable-handle')
  587. el.prepend(handle)
  588. // @ts-ignore
  589. const layoutCore = window.frontend.modules.layout.core
  590. const dispose = layoutCore.setup_resizable_container_BANG_(el,
  591. !key ? this._persistMainUILayoutData.bind(this) : () => {})
  592. ds.inited_resizable = 'true'
  593. return dispose
  594. }
  595. async load (
  596. opts?: Partial<{
  597. indicator: DeferredActor,
  598. reload: boolean
  599. }>
  600. ) {
  601. if (this.pending) {
  602. return
  603. }
  604. this._status = PluginLocalLoadStatus.LOADING
  605. this._loadErr = undefined
  606. try {
  607. // if (!this.options.entry) { // Themes package no entry field
  608. // }
  609. let installPackageThemes = await this._preparePackageConfigs()
  610. this._dispose(
  611. await this._setupUserSettings(opts?.reload)
  612. )
  613. if (!this.disabled) {
  614. await installPackageThemes.call(null)
  615. }
  616. if (this.disabled || !this.options.entry) {
  617. return
  618. }
  619. await this._tryToNormalizeEntry()
  620. this._caller = new LSPluginCaller(this)
  621. await this._caller.connectToChild()
  622. const readyFn = () => {
  623. this._caller?.callUserModel(LSPMSG_READY, { pid: this.id })
  624. }
  625. if (opts?.indicator) {
  626. opts.indicator.promise.then(readyFn)
  627. } else {
  628. readyFn()
  629. }
  630. this._dispose(async () => {
  631. await this._caller?.destroy()
  632. })
  633. } catch (e) {
  634. debug('[Load Plugin Error] ', e)
  635. this.logger?.error(e)
  636. this._status = PluginLocalLoadStatus.ERROR
  637. this._loadErr = e
  638. } finally {
  639. if (!this._loadErr) {
  640. if (this.disabled) {
  641. this._status = PluginLocalLoadStatus.UNLOADED
  642. } else {
  643. this._status = PluginLocalLoadStatus.LOADED
  644. }
  645. }
  646. }
  647. }
  648. async reload () {
  649. if (this.pending) {
  650. return
  651. }
  652. this._ctx.emit('beforereload', this)
  653. await this.unload()
  654. await this.load({ reload: true })
  655. this._ctx.emit('reloaded', this)
  656. }
  657. /**
  658. * @param unregister If true delete plugin files
  659. */
  660. async unload (unregister: boolean = false) {
  661. if (this.pending) {
  662. return
  663. }
  664. if (unregister) {
  665. await this.unload()
  666. if (this.isInstalledInDotRoot) {
  667. this._ctx.emit('unlink-plugin', this.id)
  668. }
  669. return
  670. }
  671. try {
  672. this._status = PluginLocalLoadStatus.UNLOADING
  673. const eventBeforeUnload = { unregister }
  674. // sync call
  675. try {
  676. await this._caller?.callUserModel(AWAIT_LSPMSGFn(LSPMSG_BEFORE_UNLOAD), eventBeforeUnload)
  677. this.emit('beforeunload', eventBeforeUnload)
  678. } catch (e) {
  679. console.error('[beforeunload Error]', e)
  680. }
  681. await this.dispose()
  682. this.emit('unloaded')
  683. } catch (e) {
  684. debug('[plugin unload Error]', e)
  685. return false
  686. } finally {
  687. this._status = PluginLocalLoadStatus.UNLOADED
  688. }
  689. }
  690. private async dispose () {
  691. for (const fn of this._disposes) {
  692. try {
  693. fn && (await fn())
  694. } catch (e) {
  695. console.error(this.debugTag, 'dispose Error', e)
  696. }
  697. }
  698. // clear
  699. this._disposes = []
  700. }
  701. _dispose (fn: any) {
  702. if (!fn) return
  703. this._disposes.push(fn)
  704. }
  705. _onHostMounted (callback: () => void) {
  706. const actor = this._ctx.hostMountedActor
  707. if (!actor || actor.settled) {
  708. callback()
  709. } else {
  710. actor?.promise.then(callback)
  711. }
  712. }
  713. get layoutCore (): any {
  714. // @ts-ignore
  715. return window.frontend.modules.layout.core
  716. }
  717. get isInstalledInDotRoot () {
  718. const dotRoot = this.dotConfigRoot
  719. const plgRoot = this.localRoot
  720. return dotRoot && plgRoot && plgRoot.startsWith(dotRoot)
  721. }
  722. get loaded () {
  723. return this._status === PluginLocalLoadStatus.LOADED
  724. }
  725. get pending () {
  726. return [PluginLocalLoadStatus.LOADING, PluginLocalLoadStatus.UNLOADING]
  727. .includes(this._status)
  728. }
  729. get status (): PluginLocalLoadStatus {
  730. return this._status
  731. }
  732. get settings () {
  733. return this.options.settings
  734. }
  735. get logger () {
  736. return this.options.logger
  737. }
  738. get disabled () {
  739. return this.settings?.get('disabled')
  740. }
  741. get caller () {
  742. return this._caller
  743. }
  744. get id (): string {
  745. return this._id
  746. }
  747. get shadow (): boolean {
  748. return this.options.mode === 'shadow'
  749. }
  750. get options (): PluginLocalOptions {
  751. return this._options
  752. }
  753. get themeMgr (): ILSPluginThemeManager {
  754. return this._themeMgr
  755. }
  756. get debugTag () {
  757. const name = this._options?.name
  758. return `#${this._id} ${name ?? ''}`
  759. }
  760. get localRoot (): string {
  761. return this._localRoot || this._options.url
  762. }
  763. get loadErr (): Error | undefined {
  764. return this._loadErr
  765. }
  766. get dotConfigRoot () {
  767. return path.normalize(this._ctx.options.dotConfigRoot)
  768. }
  769. get dotSettingsFile (): string | undefined {
  770. return this._dotSettingsFile
  771. }
  772. get dotPluginsRoot () {
  773. return path.join(this.dotConfigRoot, DIR_PLUGINS)
  774. }
  775. toJSON () {
  776. const json = { ...this.options } as any
  777. json.id = this.id
  778. json.err = this.loadErr
  779. json.usf = this.dotSettingsFile
  780. json.iir = this.isInstalledInDotRoot
  781. json.lsr = this._resolveResourceFullUrl('')
  782. return json
  783. }
  784. }
  785. /**
  786. * Host plugin core
  787. */
  788. class LSPluginCore
  789. extends EventEmitter<'beforeenable' | 'enabled' | 'beforedisable' | 'disabled' | 'registered' | 'error' | 'unregistered' |
  790. 'theme-changed' | 'theme-selected' | 'settings-changed' | 'unlink-plugin' | 'beforereload' | 'reloaded'>
  791. implements ILSPluginThemeManager {
  792. private _isRegistering = false
  793. private _readyIndicator?: DeferredActor
  794. private _hostMountedActor: DeferredActor = deferred()
  795. private _userPreferences: Partial<UserPreferences> = {}
  796. private _registeredThemes = new Map<PluginLocalIdentity, Array<ThemeOptions>>()
  797. private _registeredPlugins = new Map<PluginLocalIdentity, PluginLocal>()
  798. private _currentTheme: { dis: () => void, pid: PluginLocalIdentity, opt: ThemeOptions }
  799. /**
  800. * @param _options
  801. */
  802. constructor (private _options: Partial<LSPluginCoreOptions>) {
  803. super()
  804. }
  805. async loadUserPreferences () {
  806. try {
  807. const settings = await invokeHostExportedApi(`load_user_preferences`)
  808. if (settings) {
  809. Object.assign(this._userPreferences, settings)
  810. }
  811. } catch (e) {
  812. debug('[load user preferences Error]', e)
  813. }
  814. }
  815. async saveUserPreferences (settings: Partial<UserPreferences>) {
  816. try {
  817. if (settings) {
  818. Object.assign(this._userPreferences, settings)
  819. }
  820. await invokeHostExportedApi(`save_user_preferences`, this._userPreferences)
  821. } catch (e) {
  822. debug('[save user preferences Error]', e)
  823. }
  824. }
  825. async activateUserPreferences () {
  826. const { theme } = this._userPreferences
  827. // 0. theme
  828. if (theme) {
  829. await this.selectTheme(theme, false)
  830. }
  831. }
  832. /**
  833. * @param plugins
  834. * @param initial
  835. */
  836. async register (
  837. plugins: Array<RegisterPluginOpts> | RegisterPluginOpts,
  838. initial = false
  839. ) {
  840. if (!Array.isArray(plugins)) {
  841. await this.register([plugins])
  842. return
  843. }
  844. const perfTable = new Map<string, { o: PluginLocal, s: number, e: number }>()
  845. const debugPerfInfo = () => {
  846. const data = Array.from(perfTable.values()).reduce((ac, it) => {
  847. const { options, status, disabled } = it.o
  848. ac[it.o.id] = {
  849. name: options.name,
  850. entry: options.entry,
  851. status: status,
  852. enabled: typeof disabled === 'boolean' ? (!disabled ? '🟢' : '⚫️') : '🔴',
  853. perf: !it.e ? it.o.loadErr : `${(it.e - it.s).toFixed(2)}ms`
  854. }
  855. return ac
  856. }, {})
  857. console.table(data)
  858. }
  859. // @ts-ignore
  860. window.__debugPluginsPerfInfo = debugPerfInfo
  861. try {
  862. this._isRegistering = true
  863. const userConfigRoot = this._options.dotConfigRoot
  864. const readyIndicator = this._readyIndicator = deferred()
  865. await this.loadUserPreferences()
  866. const externals = new Set(this._userPreferences.externals || [])
  867. if (initial) {
  868. plugins = plugins.concat([...externals].filter(url => {
  869. return !plugins.length || (plugins as RegisterPluginOpts[]).every((p) => !p.entry && (p.url !== url))
  870. }).map(url => ({ url })))
  871. }
  872. for (const pluginOptions of plugins) {
  873. const { url } = pluginOptions as PluginLocalOptions
  874. const pluginLocal = new PluginLocal(pluginOptions as PluginLocalOptions, this, this)
  875. const perfInfo = { o: pluginLocal, s: performance.now(), e: 0 }
  876. perfTable.set(pluginLocal.id, perfInfo)
  877. await pluginLocal.load({ indicator: readyIndicator })
  878. const { loadErr } = pluginLocal
  879. if (loadErr) {
  880. debug(`[Failed LOAD Plugin] #`, pluginOptions)
  881. this.emit('error', loadErr)
  882. if (
  883. loadErr instanceof IllegalPluginPackageError ||
  884. loadErr instanceof ExistedImportedPluginPackageError) {
  885. // TODO: notify global log system?
  886. continue
  887. }
  888. }
  889. perfInfo.e = performance.now()
  890. pluginLocal.settings?.on('change', (a) => {
  891. this.emit('settings-changed', pluginLocal.id, a)
  892. pluginLocal.caller?.callUserModel(LSPMSG_SETTINGS, { payload: a })
  893. })
  894. this._registeredPlugins.set(pluginLocal.id, pluginLocal)
  895. this.emit('registered', pluginLocal)
  896. // external plugins
  897. if (!pluginLocal.isInstalledInDotRoot) {
  898. externals.add(url)
  899. }
  900. }
  901. await this.saveUserPreferences({ externals: Array.from(externals) })
  902. await this.activateUserPreferences()
  903. readyIndicator.resolve('ready')
  904. } catch (e) {
  905. console.error(e)
  906. } finally {
  907. this._isRegistering = false
  908. debugPerfInfo()
  909. }
  910. }
  911. async reload (plugins: Array<PluginLocalIdentity> | PluginLocalIdentity) {
  912. if (!Array.isArray(plugins)) {
  913. await this.reload([plugins])
  914. return
  915. }
  916. for (const identity of plugins) {
  917. try {
  918. const p = this.ensurePlugin(identity)
  919. await p.reload()
  920. } catch (e) {
  921. debug(e)
  922. }
  923. }
  924. }
  925. async unregister (plugins: Array<PluginLocalIdentity> | PluginLocalIdentity) {
  926. if (!Array.isArray(plugins)) {
  927. await this.unregister([plugins])
  928. return
  929. }
  930. const unregisteredExternals: Array<string> = []
  931. for (const identity of plugins) {
  932. const p = this.ensurePlugin(identity)
  933. if (!p.isInstalledInDotRoot) {
  934. unregisteredExternals.push(p.options.url)
  935. }
  936. await p.unload(true)
  937. this._registeredPlugins.delete(identity)
  938. this.emit('unregistered', identity)
  939. }
  940. let externals = this._userPreferences.externals || []
  941. if (externals.length && unregisteredExternals.length) {
  942. await this.saveUserPreferences({
  943. externals: externals.filter((it) => {
  944. return !unregisteredExternals.includes(it)
  945. })
  946. })
  947. }
  948. }
  949. async enable (plugin: PluginLocalIdentity) {
  950. const p = this.ensurePlugin(plugin)
  951. if (p.pending) return
  952. this.emit('beforeenable')
  953. p.settings?.set('disabled', false)
  954. this.emit('enabled', p.id)
  955. }
  956. async disable (plugin: PluginLocalIdentity) {
  957. const p = this.ensurePlugin(plugin)
  958. if (p.pending) return
  959. this.emit('beforedisable')
  960. p.settings?.set('disabled', true)
  961. this.emit('disabled', p.id)
  962. }
  963. async _hook (ns: string, type: string, payload?: any, pid?: string) {
  964. for (const [_, p] of this._registeredPlugins) {
  965. if (!pid || pid === p.id) {
  966. p.caller?.callUserModel(LSPMSG, {
  967. ns, type: snakeCase(type), payload
  968. })
  969. }
  970. }
  971. }
  972. hookApp (type: string, payload?: any, pid?: string) {
  973. this._hook(`hook:app`, type, payload, pid)
  974. }
  975. hookEditor (type: string, payload?: any, pid?: string) {
  976. this._hook(`hook:editor`, type, payload, pid)
  977. }
  978. _execDirective (tag: string, ...params: any[]) {
  979. }
  980. ensurePlugin (plugin: PluginLocalIdentity | PluginLocal) {
  981. if (plugin instanceof PluginLocal) {
  982. return plugin
  983. }
  984. const p = this._registeredPlugins.get(plugin)
  985. if (!p) {
  986. throw new Error(`plugin #${plugin} not existed.`)
  987. }
  988. return p
  989. }
  990. hostMounted () {
  991. this._hostMountedActor.resolve()
  992. }
  993. get registeredPlugins (): Map<PluginLocalIdentity, PluginLocal> {
  994. return this._registeredPlugins
  995. }
  996. get options () {
  997. return this._options
  998. }
  999. get readyIndicator (): DeferredActor | undefined {
  1000. return this._readyIndicator
  1001. }
  1002. get hostMountedActor (): DeferredActor {
  1003. return this._hostMountedActor
  1004. }
  1005. get isRegistering (): boolean {
  1006. return this._isRegistering
  1007. }
  1008. get themes (): Map<PluginLocalIdentity, Array<ThemeOptions>> {
  1009. return this._registeredThemes
  1010. }
  1011. async registerTheme (id: PluginLocalIdentity, opt: ThemeOptions): Promise<void> {
  1012. debug('registered Theme #', id, opt)
  1013. if (!id) return
  1014. let themes: Array<ThemeOptions> = this._registeredThemes.get(id)!
  1015. if (!themes) {
  1016. this._registeredThemes.set(id, themes = [])
  1017. }
  1018. themes.push(opt)
  1019. this.emit('theme-changed', this.themes, { id, ...opt })
  1020. }
  1021. async selectTheme (opt?: ThemeOptions, effect = true): Promise<void> {
  1022. // clear current
  1023. if (this._currentTheme) {
  1024. this._currentTheme.dis?.()
  1025. }
  1026. const disInjectedTheme = setupInjectedTheme(opt?.url)
  1027. this.emit('theme-selected', opt)
  1028. effect && await this.saveUserPreferences({ theme: opt?.url ? opt : null })
  1029. if (opt?.url) {
  1030. this._currentTheme = {
  1031. dis: () => {
  1032. disInjectedTheme()
  1033. effect && this.saveUserPreferences({ theme: null })
  1034. }, opt, pid: opt.pid
  1035. }
  1036. }
  1037. }
  1038. async unregisterTheme (id: PluginLocalIdentity, effect: boolean = true): Promise<void> {
  1039. debug('unregistered Theme #', id)
  1040. if (!this._registeredThemes.has(id)) return
  1041. this._registeredThemes.delete(id)
  1042. this.emit('theme-changed', this.themes, { id })
  1043. if (effect && this._currentTheme?.pid == id) {
  1044. this._currentTheme.dis?.()
  1045. this._currentTheme = null
  1046. // reset current theme
  1047. this.emit('theme-selected', null)
  1048. }
  1049. }
  1050. }
  1051. function setupPluginCore (options: any) {
  1052. const pluginCore = new LSPluginCore(options)
  1053. debug('=== 🔗 Setup Logseq Plugin System 🔗 ===')
  1054. window.LSPluginCore = pluginCore
  1055. }
  1056. export {
  1057. PluginLocal,
  1058. pluginHelpers,
  1059. setupPluginCore
  1060. }