LSPlugin.core.ts 37 KB

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