Browse Source

Merge pull request #11664 from logseq/enhance/plugin-web

Feat: support plugins for web
Tienson Qin 9 months ago
parent
commit
28ae59da8f

+ 74 - 42
libs/src/LSPlugin.core.ts

@@ -98,11 +98,13 @@ class PluginSettings extends EventEmitter<'change' | 'reset'> {
       return
     }
 
-    this.emit('change', Object.assign({}, this._settings), o)
+    this.emit('change', { ...this._settings }, o)
   }
 
   set settings(value: Record<string, any>) {
-    this._settings = value
+    const o = deepMerge({}, this._settings)
+    this._settings = value || {}
+    this.emit('change', { ...this._settings }, o)
   }
 
   get settings(): Record<string, any> {
@@ -153,6 +155,7 @@ interface PluginLocalOptions {
   name: string
   version: string
   mode: 'shadow' | 'iframe'
+  webPkg?: any // web plugin package.json data
   settingsSchema?: SettingSchemaDesc[]
   settings?: PluginSettings
   effect?: boolean
@@ -506,40 +509,45 @@ class PluginLocal extends EventEmitter<
   _resolveResourceFullUrl(filePath: string, localRoot?: string) {
     if (!filePath?.trim()) return
     localRoot = localRoot || this._localRoot
+
+    if (this.isWebPlugin) {
+      // TODO: strategy for Logseq plugins center
+      return `https://pub-80f42b85b62c40219354a834fcf2bbfa.r2.dev/${path.join(localRoot, filePath)}`
+    }
+
     const reg = /^(http|file)/
     if (!reg.test(filePath)) {
       const url = path.join(localRoot, filePath)
       filePath = reg.test(url) ? url : PROTOCOL_FILE + url
     }
-    return !this.options.effect && this.isInstalledInDotRoot
+    return !this.options.effect && this.isInstalledInLocalDotRoot
       ? convertToLSPResource(filePath, this.dotPluginsRoot)
       : filePath
   }
 
   async _preparePackageConfigs() {
-    const { url } = this._options
-    let pkg: any
+    const { url, webPkg } = this._options
+    let pkg: any = webPkg
 
-    try {
-      if (!url) {
-        throw new Error('Can not resolve package config location')
-      }
+    if (!pkg) {
+      try {
+        if (!url) {
+          throw new Error('Can not resolve package config location')
+        }
 
-      debug('prepare package root', url)
+        debug('prepare package root', url)
 
-      pkg = await invokeHostExportedApi('load_plugin_config', url)
+        pkg = await invokeHostExportedApi('load_plugin_config', url)
 
-      if (!pkg || ((pkg = JSON.parse(pkg)), !pkg)) {
-        throw new Error(`Parse package config error #${url}/package.json`)
+        if (!pkg || ((pkg = JSON.parse(pkg)), !pkg)) {
+          throw new Error(`Parse package config error #${url}/package.json`)
+        }
+      } catch (e) {
+        throw new IllegalPluginPackageError(e.message)
       }
-    } catch (e) {
-      throw new IllegalPluginPackageError(e.message)
     }
 
-    const localRoot = (this._localRoot = safetyPathNormalize(url))
-    const logseq: Partial<LSPluginPkgConfig> = pkg.logseq || {}
-
-      // Pick legal attrs
+    // Pick legal attrs
     ;[
       'name',
       'author',
@@ -551,18 +559,24 @@ class PluginLocal extends EventEmitter<
       'effect',
       'sponsors',
     ]
-      .concat(!this.isInstalledInDotRoot ? ['devEntry'] : [])
+      .concat(!this.isInstalledInLocalDotRoot ? ['devEntry'] : [])
       .forEach((k) => {
         this._options[k] = pkg[k]
       })
 
+    const { repo, version } = this._options
+    const localRoot = (this._localRoot = this.isWebPlugin ? `${repo}/${version}` : safetyPathNormalize(url))
+    const logseq: Partial<LSPluginPkgConfig> = pkg.logseq || {}
     const validateEntry = (main) => main && /\.(js|html)$/.test(main)
 
     // Entry from main
     const entry = logseq.entry || logseq.main || pkg.main
+
     if (validateEntry(entry)) {
       // Theme has no main
       this._options.entry = this._resolveResourceFullUrl(entry, localRoot)
+
+      // development mode entry
       this._options.devEntry = logseq.devEntry
 
       if (logseq.mode) {
@@ -577,16 +591,16 @@ class PluginLocal extends EventEmitter<
     this._options.icon = icon && this._resolveResourceFullUrl(icon)
     this._options.theme = Boolean(logseq.theme || !!logseq.themes)
 
-    // TODO: strategy for Logseq plugins center
-    if (this.isInstalledInDotRoot) {
+    if (this.isInstalledInLocalDotRoot) {
       this._id = path.basename(localRoot)
-    } else {
+    } else if (!this.isWebPlugin) {
+      // development mode
       if (logseq.id) {
         this._id = logseq.id
       } else {
         logseq.id = this.id
         try {
-          await invokeHostExportedApi('save_plugin_config', url, {
+          await invokeHostExportedApi('save_plugin_package_json', url, {
             ...pkg,
             logseq,
           })
@@ -631,7 +645,7 @@ class PluginLocal extends EventEmitter<
 
     let dirPathInstalled = null
     let tmp_file_method = 'write_user_tmp_file'
-    if (this.isInstalledInDotRoot) {
+    if (this.isInstalledInLocalDotRoot) {
       tmp_file_method = 'write_dotdir_file'
       dirPathInstalled = this._localRoot.replace(this.dotPluginsRoot, '')
       dirPathInstalled = path.join(DIR_PLUGINS, dirPathInstalled)
@@ -674,9 +688,10 @@ class PluginLocal extends EventEmitter<
       if (!options.url) return
 
       if (!options.url.startsWith('http') && this._localRoot) {
-        options.url = path.join(this._localRoot, options.url)
+        options.url = this._resolveResourceFullUrl(options.url, this._localRoot)
+
         // file:// for native
-        if (!options.url.startsWith('file:')) {
+        if (!this.isWebPlugin && !options.url.startsWith('file:')) {
           options.url = 'assets://' + options.url
         }
       }
@@ -850,6 +865,8 @@ class PluginLocal extends EventEmitter<
         return
       }
 
+      this._ctx.emit('beforeload', this)
+
       await this._tryToNormalizeEntry()
 
       this._caller = new LSPluginCaller(this)
@@ -870,6 +887,8 @@ class PluginLocal extends EventEmitter<
       })
 
       this._dispose(cleanInjectedScripts.bind(this))
+
+      this._ctx.emit('loadeded', this)
     } catch (e) {
       this.logger.error('load', e, true)
 
@@ -909,7 +928,7 @@ class PluginLocal extends EventEmitter<
     if (unregister) {
       await this.unload()
 
-      if (this.isInstalledInDotRoot) {
+      if (this.isWebPlugin || this.isInstalledInLocalDotRoot) {
         this._ctx.emit('unlink-plugin', this.id)
       }
 
@@ -971,12 +990,17 @@ class PluginLocal extends EventEmitter<
     }
   }
 
+  get isWebPlugin() {
+    return this._ctx.isWebPlatform || !!this.options.webPkg
+  }
+
   get layoutCore(): any {
     // @ts-expect-error
     return window.frontend.modules.layout.core
   }
 
-  get isInstalledInDotRoot() {
+  get isInstalledInLocalDotRoot() {
+    if (this.isWebPlugin) return false
     const dotRoot = this.dotConfigRoot
     const plgRoot = this.localRoot
     return dotRoot && plgRoot && plgRoot.startsWith(dotRoot)
@@ -1074,14 +1098,19 @@ class PluginLocal extends EventEmitter<
     this._sdk = value
   }
 
-  toJSON() {
+  toJSON(settings = true) {
     const json = { ...this.options } as any
     json.id = this.id
     json.err = this.loadErr
     json.usf = this.dotSettingsFile
-    json.iir = this.isInstalledInDotRoot
+    json.iir = this.isInstalledInLocalDotRoot
     json.lsr = this._resolveResourceFullUrl('/')
-    json.settings = json.settings?.toJSON()
+
+    if (settings === false) {
+      delete json.settings
+    } else {
+      json.settings = json.settings?.toJSON()
+    }
 
     return json
   }
@@ -1105,6 +1134,8 @@ class LSPluginCore
     | 'reset-custom-theme'
     | 'settings-changed'
     | 'unlink-plugin'
+    | 'beforeload'
+    | 'loadeded'
     | 'beforereload'
     | 'reloaded'
   >
@@ -1181,10 +1212,10 @@ class LSPluginCore
 
     // If there is currently a theme that has been set
     if (currentTheme) {
-      await this.selectTheme(currentTheme, { effect: false })
+      await this.selectTheme(currentTheme, { effect: false, emit: false })
     } else if (legacyTheme) {
       // Otherwise compatible with older versions
-      await this.selectTheme(legacyTheme, { effect: false })
+      await this.selectTheme(legacyTheme, { effect: false, emit: false })
     }
   }
 
@@ -1235,7 +1266,7 @@ class LSPluginCore
     try {
       this._isRegistering = true
 
-      const userConfigRoot = this._options.dotConfigRoot
+      const _userConfigRoot = this._options.dotConfigRoot
       const readyIndicator = (this._readyIndicator = deferred())
 
       await this.loadUserPreferences()
@@ -1318,7 +1349,7 @@ class LSPluginCore
         this.emit('registered', pluginLocal)
 
         // external plugins
-        if (!pluginLocal.isInstalledInDotRoot) {
+        if (!pluginLocal.isWebPlugin && !pluginLocal.isInstalledInLocalDotRoot) {
           externals.add(url)
         }
       }
@@ -1363,7 +1394,7 @@ class LSPluginCore
     for (const identity of plugins) {
       const p = this.ensurePlugin(identity)
 
-      if (!p.isInstalledInDotRoot) {
+      if (!p.isWebPlugin && !p.isInstalledInLocalDotRoot) {
         unregisteredExternals.push(p.options.url)
       }
 
@@ -1488,6 +1519,10 @@ class LSPluginCore
     return cleanInjectedUI(id)
   }
 
+  get isWebPlatform() {
+    return this.options.dotConfigRoot?.startsWith('LSPUserDotRoot')
+  }
+
   get registeredPlugins(): Map<PluginLocalIdentity, PluginLocal> {
     return this._registeredPlugins
   }
@@ -1543,10 +1578,7 @@ class LSPluginCore
     } = {}
   ) {
     const { effect, emit } = Object.assign(
-      {},
-      { effect: true, emit: true },
-      options
-    )
+      { effect: true, emit: true }, options)
 
     // Clear current theme before injecting.
     if (this._currentTheme) {
@@ -1581,7 +1613,7 @@ class LSPluginCore
     }
 
     if (emit) {
-      this.emit('theme-selected', theme)
+      this.emit('theme-selected', theme, options)
     }
   }
 

+ 1 - 1
libs/yarn.lock

@@ -1078,7 +1078,7 @@
   dependencies:
     "@types/ms" "*"
 
-"@types/dompurify@2.5.4":
+"@types/[email protected].0":
   version "2.4.0"
   resolved "https://registry.yarnpkg.com/@types/dompurify/-/dompurify-2.4.0.tgz#fd9706392a88e0e0e6d367f3588482d817df0ab9"
   integrity sha512-IDBwO5IZhrKvHFUl+clZxgf3hn2b/lU6H1KaBShPkQyGJUQ0xwebezIPSuiyGwfz1UzJWQl4M7BDxtHtCCPlTg==

+ 1 - 0
package.json

@@ -20,6 +20,7 @@
         "gulp-replace": "^1.1.4",
         "gulp-postcss": "^10.0.0",
         "ip": "1.1.9",
+        "semver": "7.5.2",
         "karma": "^6.4.4",
         "karma-chrome-launcher": "^3.2.0",
         "karma-cljs-test": "^0.1.0",

+ 1 - 0
public/index.html

@@ -50,6 +50,7 @@
 <script defer src="/static/js/highlight.min.js"></script>
 <script defer src="/static/js/interact.min.js"></script>
 <script defer src="/static/js/marked.min.js"></script>
+<script defer src="/static/js/eventemitter3.umd.min.js"></script>
 <script defer src="/static/js/html2canvas.min.js"></script>
 <script defer src="/static/js/react.production.min.js"></script>
 <script defer src="/static/js/react-dom.production.min.js"></script>

+ 1 - 0
resources/index.html

@@ -49,6 +49,7 @@ const portal = new MagicPortal(worker);
 <script defer src="./js/highlight.min.js"></script>
 <script defer src="./js/interact.min.js"></script>
 <script defer src="./js/marked.min.js"></script>
+<script defer src="./js/eventemitter3.umd.min.js"></script>
 <script defer src="./js/html2canvas.min.js"></script>
 <script defer src="./js/lsplugin.core.js"></script>
 <script defer src="./js/react.production.min.js"></script>

File diff suppressed because it is too large
+ 0 - 0
resources/js/eventemitter3.umd.min.js


File diff suppressed because it is too large
+ 0 - 1
resources/js/lsplugin.core.js


+ 2 - 2
src/electron/electron/handler.cljs

@@ -467,8 +467,8 @@
   nil)
 
 
-(defmethod handle :installMarketPlugin [_ [_ mft]]
-  (plugin/install-or-update! mft))
+(defmethod handle :installMarketPlugin [_ [_ manifest]]
+  (plugin/install-or-update! manifest))
 
 (defmethod handle :updateMarketPlugin [_ [_ pkg]]
   (plugin/install-or-update! pkg))

+ 117 - 98
src/main/frontend/components/plugins.cljs

@@ -200,9 +200,9 @@
        :dangerouslySetInnerHTML {:__html content}}]]))
 
 (rum/defc remote-readme-display
-  [repo _content]
+  [{:keys [repo]} _content]
 
-  (let [src (str "lsp://logseq.com/marketplace.html?repo=" repo)]
+  (let [src (str "./marketplace.html?repo=" repo)]
     [:iframe.lsp-frame-readme {:src src}]))
 
 (defn security-warning
@@ -231,7 +231,7 @@
     [:a.btn
      {:class    (util/classnames [{:disabled   (or installed? installing-or-updating?)
                                    :installing installing-or-updating?}])
-      :on-click #(plugin-common-handler/install-marketplace-plugin item)}
+      :on-click #(plugin-common-handler/install-marketplace-plugin! item)}
      (if installed?
        (t :plugin/installed)
        (if installing-or-updating?
@@ -249,24 +249,27 @@
      [:strong (ui/icon "settings")]
      [:ul.menu-list
       [:li {:on-click #(plugin-handler/open-plugin-settings! id false)} (t :plugin/open-settings)]
-      [:li {:on-click #(js/apis.openPath url)} (t :plugin/open-package)]
+      (when (util/electron?)
+        [:li {:on-click #(js/apis.openPath url)} (t :plugin/open-package)])
       [:li {:on-click #(plugin-handler/open-report-modal! id name)} (t :plugin/report-security)]
       [:li {:on-click
             #(-> (shui/dialog-confirm!
                   [:b (t :plugin/delete-alert name)])
                  (p/then (fn []
-                           (plugin-common-handler/unregister-plugin id)
-                           (plugin-config-handler/remove-plugin id))))}
+                          (plugin-common-handler/unregister-plugin id)
+
+                          (when (util/electron?)
+                           (plugin-config-handler/remove-plugin id)))))}
        (t :plugin/uninstall)]]]
 
     (when (seq sponsors)
-      [:div.de.sponsors
-       [:strong (ui/icon "coffee")]
-       [:ul.menu-list
-        (for [link sponsors]
-          [:li {:key link}
-           [:a {:href link :target "_blank"}
-            [:span.flex.items-center link (ui/icon "external-link")]]])]])]
+     [:div.de.sponsors
+      [:strong (ui/icon "coffee")]
+      [:ul.menu-list
+       (for [link sponsors]
+        [:li {:key link}
+         [:a {:href link :target "_blank"}
+          [:span.flex.items-center link (ui/icon "external-link")]]])]])]
 
    [:div.r.flex.items-center
     (when (and unpacked? (not disabled?))
@@ -297,74 +300,75 @@
                true)]])
 
 (defn get-open-plugin-readme-handler
-  [url item repo]
+  [url {:keys [webPkg] :as item} repo]
   #(plugin-handler/open-readme!
-    url item (if repo remote-readme-display local-markdown-display)))
+    url item (if (or repo webPkg) remote-readme-display local-markdown-display)))
 
 (rum/defc plugin-item-card < rum/static
-  [t {:keys [id name title version url description author icon iir repo sponsors] :as item}
-   disabled? market? *search-key has-other-pending?
-   installing-or-updating? installed? stat coming-update]
-
-  (let [name        (or title name "Untitled")
-        unpacked?   (not iir)
-        new-version (state/coming-update-new-version? coming-update)]
-    [:div.cp__plugins-item-card
-     {:key   (str "lsp-card-" id)
-      :class (util/classnames
-              [{:market          market?
-                :installed       installed?
-                :updating        installing-or-updating?
-                :has-new-version new-version}])}
-
-     [:div.l.link-block.cursor-pointer
+ [t {:keys [id name title version url description author icon iir repo sponsors webPkg] :as item}
+  disabled? market? *search-key has-other-pending?
+  installing-or-updating? installed? stat coming-update]
+
+ (let [name (or title name "Untitled")
+       web? (not (nil? webPkg))
+       unpacked? (and (not web?) (not iir))
+       new-version (state/coming-update-new-version? coming-update)]
+  [:div.cp__plugins-item-card
+   {:key   (str "lsp-card-" id)
+    :class (util/classnames
+            [{:market          market?
+              :installed       installed?
+              :updating        installing-or-updating?
+              :has-new-version new-version}])}
+
+   [:div.l.link-block.cursor-pointer
+    {:on-click (get-open-plugin-readme-handler url item repo)}
+    (if (and icon (not (string/blank? icon)))
+     [:img.icon {:src (if market? (plugin-handler/pkg-asset id icon) icon)}]
+     svg/folder)
+
+    (when (and (not market?) unpacked?)
+     [:span.flex.justify-center.text-xs.text-error.pt-2 (t :plugin/unpacked)])]
+
+   [:div.r
+    [:h3.head.text-xl.font-bold.pt-1.5
+
+     [:span.l.link-block.cursor-pointer
       {:on-click (get-open-plugin-readme-handler url item repo)}
-      (if (and icon (not (string/blank? icon)))
-        [:img.icon {:src (if market? (plugin-handler/pkg-asset id icon) icon)}]
-        svg/folder)
-
-      (when (and (not market?) unpacked?)
-        [:span.flex.justify-center.text-xs.text-error.pt-2 (t :plugin/unpacked)])]
-
-     [:div.r
-      [:h3.head.text-xl.font-bold.pt-1.5
-
-       [:span.l.link-block.cursor-pointer
-        {:on-click (get-open-plugin-readme-handler url item repo)}
-        name]
-       (when (not market?) [:sup.inline-block.px-1.text-xs.opacity-50 version])]
-
-      [:div.desc.text-xs.opacity-70
-       [:p description]
-       ;;[:small (js/JSON.stringify (bean/->js settings))]
-       ]
-
-      ;; Author & Identity
-      [:div.flag
-       [:p.text-xs.pr-2.flex.justify-between
-        [:small {:on-click #(when-let [^js el (js/document.querySelector ".cp__plugins-page .search-ctls input")]
-                              (reset! *search-key (str "@" author))
-                              (.select el))} author]
-        [:small {:on-click #(do
-                              (notification/show! "Copied!" :success)
-                              (util/copy-to-clipboard! id))}
-         (str "ID: " id)]]]
-
-      ;; Github repo
-      [:div.flag.is-top.opacity-50
-       (when repo
-         [:a.flex {:target "_blank"
-                   :href   (plugin-handler/gh-repo-url repo)}
-          (svg/github {:width 16 :height 16})])]
-
-      (if market?
-        ;; market ctls
-        (card-ctls-of-market item stat installed? installing-or-updating?)
-
-        ;; installed ctls
-        (card-ctls-of-installed
-         id name url sponsors unpacked? disabled?
-         installing-or-updating? has-other-pending? new-version item))]]))
+      name]
+     (when (not market?) [:sup.inline-block.px-1.text-xs.opacity-50 version])]
+
+    [:div.desc.text-xs.opacity-70
+     [:p description]
+     ;;[:small (js/JSON.stringify (bean/->js settings))]
+     ]
+
+    ;; Author & Identity
+    [:div.flag
+     [:p.text-xs.pr-2.flex.justify-between
+      [:small {:on-click #(when-let [^js el (js/document.querySelector ".cp__plugins-page .search-ctls input")]
+                           (reset! *search-key (str "@" author))
+                           (.select el))} author]
+      [:small {:on-click #(do
+                           (notification/show! "Copied!" :success)
+                           (util/copy-to-clipboard! id))}
+       (str "ID: " id)]]]
+
+    ;; Github repo
+    [:div.flag.is-top.opacity-50
+     (when repo
+      [:a.flex {:target "_blank"
+                :href   (plugin-handler/gh-repo-url repo)}
+       (svg/github {:width 16 :height 16})])]
+
+    (if market?
+     ;; market ctls
+     (card-ctls-of-market item stat installed? installing-or-updating?)
+
+     ;; installed ctls
+     (card-ctls-of-installed
+      id name url sponsors unpacked? disabled?
+      installing-or-updating? has-other-pending? new-version item))]]))
 
 (rum/defc panel-tab-search < rum/static
   [search-key *search-key *search-ref]
@@ -501,7 +505,7 @@
      [:div.flex.items-center.l
       (category-tabs t total-nums category #(reset! *category %))
 
-      (when (and develop-mode? (not market?))
+      (when (and develop-mode? (util/electron?) (not market?))
         [:div
          (ui/tippy {:html  [:div (t :plugin/unpacked-tips)]
                     :arrow true}
@@ -512,7 +516,8 @@
                      :class "load-unpacked"
                      :on-click plugin-handler/load-unpacked-plugin}))
 
-         (unpacked-plugin-loader selected-unpacked-pkg)])]
+         (when (util/electron?)
+           (unpacked-plugin-loader selected-unpacked-pkg))])]
 
      [:div.flex.items-center.r
       ;; extra info
@@ -600,16 +605,18 @@
                             [{:title [:span.flex.items-center.gap-1 (ui/icon "rotate-clockwise") (t :plugin/check-all-updates)]
                               :options {:on-click #(plugin-handler/user-check-enabled-for-updates! (not= :plugins category))}}])
 
-                          [{:title [:span.flex.items-center.gap-1 (ui/icon "world") (t :settings-page/network-proxy)]
-                            :options {:on-click #(state/pub-event! [:go/proxy-settings agent-opts])}}]
+                          (when (util/electron?)
+                           [{:title   [:span.flex.items-center.gap-1 (ui/icon "world") (t :settings-page/network-proxy)]
+                             :options {:on-click #(state/pub-event! [:go/proxy-settings agent-opts])}}
 
-                          [{:title [:span.flex.items-center.gap-1 (ui/icon "arrow-down-circle") (t :plugin.install-from-file/menu-title)]
-                            :options {:on-click plugin-config-handler/open-replace-plugins-modal}}]
+                            {:title   [:span.flex.items-center.gap-1 (ui/icon "arrow-down-circle") (t :plugin.install-from-file/menu-title)]
+                             :options {:on-click plugin-config-handler/open-replace-plugins-modal}}])
 
                           [{:hr true}]
 
-                          (when (state/developer-mode?)
-                            [{:title [:span.flex.items-center.gap-1 (ui/icon "file-code") (t :plugin/open-preferences)]
+                          (when (and (state/developer-mode?)
+                                     (util/electron?))
+                           [{:title [:span.flex.items-center.gap-1 (ui/icon "file-code") (t :plugin/open-preferences)]
                               :options {:on-click
                                         #(p/let [root (plugin-handler/get-ls-dotdir-root)]
                                            (js/apis.openPath (str root "/preferences.json")))}}
@@ -794,7 +801,7 @@
          (when (seq sorted-plugins)
            (lazy-items-loader load-more-pages!))]])]))
 
-(rum/defcs installed-plugins
+(rum/defcs ^:large-vars/data-var installed-plugins
   < rum/static rum/reactive
   plugin-items-list-mixins
   (rum/local "" ::search-key)
@@ -891,8 +898,11 @@
                                true nil (get coming-updates pid)))
            (:id item)))]
 
-      (when (seq sorted-plugins)
-        (lazy-items-loader load-more-pages!))]]))
+      (if (seq sorted-plugins)
+        (lazy-items-loader load-more-pages!)
+        [:div.flex.items-center.justify-center.py-28.flex-col.gap-2.opacity-30
+         (shui/tabler-icon "list-search" {:size 40})
+         [:span.text-sm "Nothing Founded."]])]]))
 
 (rum/defcs waiting-coming-updates
   < rum/reactive
@@ -1246,22 +1256,31 @@
      [market?])
 
     [:div.cp__plugins-page
-     {:ref       *el-ref
+     {:ref *el-ref
+      :class (when-not (util/electron?) "web-platform")
       :tab-index "-1"}
+
      [:h1 (t :plugins)]
-     (security-warning)
 
-     [:hr.my-4]
+     (when (util/electron?)
+       [:<>
+        (security-warning)
+        [:hr.my-4]])
 
      [:div.tabs.flex.items-center.justify-center
       [:div.tabs-inner.flex.items-center
-       (ui/button [:span.it (t :plugin/installed)]
-                  :on-click #(set-active! :installed)
-                  :intent (if-not market? "" "link"))
-
-       (ui/button [:span.mk (svg/apps 16) (t :plugin/marketplace)]
-                  :on-click #(set-active! :marketplace)
-                  :intent (if market? "" "link"))]]
+       (shui/button {:on-click #(set-active! :installed)
+                     :class (when (not market?) "active")
+                     :size :sm
+                     :variant :text}
+         (t :plugin/installed))
+
+       (shui/button {:on-click #(set-active! :marketplace)
+                     :class (when market? "active")
+                     :size :sm
+                     :variant :text}
+         (shui/tabler-icon "apps")
+         (t :plugin/marketplace))]]
 
      [:div.panels
       (if market?

+ 33 - 41
src/main/frontend/components/plugins.css

@@ -22,36 +22,29 @@
     }
 
     .tabs {
-      .ui__button {
-        margin: 0 8px;
+      &-inner {
+        @apply flex gap-3 p-1 bg-gray-02 rounded-md;
 
-        > span {
-          display: flex;
-          align-items: center;
-          font-size: 16px;
-          font-weight: normal;
+        .ui__button {
+          @apply flex items-center opacity-70 px-3;
 
-          svg {
-            margin-right: 6px;
+          &.active {
+            @apply bg-gray-04 opacity-100;
           }
         }
-
-        &.active {
-        }
       }
     }
 
     .panels {
-      margin: 0 -32px -24px;
+      @apply mx-[-20px] mt-2 -mb-2;
     }
 
     .secondary-tabs {
-      button {
-        margin-right: 5px;
-        background: transparent;
+      .ui__button {
+        @apply mr-1.5 bg-transparent border border-b-2;
 
         .ti, .tie {
-          margin-right: 3px;
+          @apply mr-1;
         }
 
         &.active {
@@ -95,26 +88,15 @@
       }
 
       &.contribute {
-        position: absolute;
-        top: -46px;
-        right: 20px;
-        background: transparent;
-        font-size: 12px;
-        opacity: .8;
-        display: none;
-
-        @screen md {
-          display: block;
-        }
+        @apply absolute top-[-42px] right-5 bg-transparent text-xs opacity-60 hidden sm:block;
       }
 
       &.load-unpacked {
-        opacity: .9;
-        background: transparent;
+        @apply opacity-90 bg-transparent;
       }
 
       &.sort-or-filter-by, &.more-do {
-        padding: 0 6px;
+        @apply py-0 px-1.5;
       }
 
       &.picked {
@@ -162,6 +144,12 @@
         }
       }
     }
+
+    &.web-platform {
+      .cp__plugins-item-lists {
+        @apply max-h-[80vh] h-[calc(100vh-320px)];
+      }
+    }
   }
 
   &-installed {
@@ -181,7 +169,7 @@
   }
 
   &-item-lists {
-    @apply w-full max-h-[56vh] overflow-y-auto;
+    @apply w-full max-h-[80vh] h-[calc(100vh-480px)] overflow-y-auto;
 
     &-inner {
       @apply grid grid-flow-row gap-3 px-4
@@ -469,25 +457,29 @@
 
   &-settings {
     &-inner {
-      position: relative;
-      padding: 10px 0 20px;
+      @apply relative pt-2.5 pb-5 px-0;
 
       > .edit-file {
-        position: absolute;
-        top: 12px;
-        right: 8px;
+        @apply absolute top-3 right-2;
+      }
+
+      &[data-mode=code] {
+        @apply pt-2;
+
+        > .edit-file {
+          @apply -top-4;
+        }
       }
 
       .heading-item {
-        margin: 12px 12px 6px;
-        border-bottom: 1px solid var(--ls-border-color, #738694);
+        @apply pt-3 px-3 pb-1.5 border-b;
 
         h2 {
-          font-weight: bold;
+          @apply font-bold;
         }
 
         small:empty {
-          display: none;
+          @apply hidden;
         }
       }
 

+ 65 - 26
src/main/frontend/components/plugins_settings.cljs

@@ -1,8 +1,11 @@
 (ns frontend.components.plugins-settings
-  (:require [rum.core :as rum]
+  (:require [logseq.shui.ui :as shui]
+            [rum.core :as rum]
             [frontend.util :as util]
             [frontend.ui :as ui]
             [frontend.handler.plugin :as plugin-handler]
+            [frontend.components.lazy-editor :as lazy-editor]
+            [frontend.handler.notification :as notification]
             [cljs-bean.core :as bean]
             [goog.functions :refer [debounce]]))
 
@@ -19,11 +22,16 @@
    {:dangerouslySetInnerHTML {:__html (dom-purify html nil)}}])
 
 (rum/defc edit-settings-file
-  [pid {:keys [class]}]
+  [pid {:keys [class edit-mode set-edit-mode!]}]
   [:a.text-sm.hover:underline
    {:class    class
-    :on-click #(plugin-handler/open-settings-file-in-default-app! pid)}
-   "Edit settings.json"])
+    :on-click (fn []
+                (if (util/electron?)
+                  (plugin-handler/open-settings-file-in-default-app! pid)
+                  (set-edit-mode! #(if % nil :code))))}
+   (if (= edit-mode :code)
+     "Exit code mode"
+     "Edit settings.json")])
 
 (rum/defc render-item-input
   [val {:keys [key type title default description inputAs]} update-setting!]
@@ -89,7 +97,8 @@
 
    [:div.form-control
     (html-content description)
-    [:div.pl-1 (edit-settings-file pid nil)]]])
+    (when (util/electron?)
+      [:div.pl-1 (edit-settings-file pid nil)])]])
 
 (rum/defc render-item-heading
   [{:keys [key title description]}]
@@ -107,14 +116,15 @@
   [schema ^js pl]
   (let [^js plugin-settings (.-settings pl)
         pid (.-id pl)
-        [settings, set-settings] (rum/use-state (bean/->clj (.toJSON plugin-settings)))
+        [settings, set-settings!] (rum/use-state (bean/->clj (.toJSON plugin-settings)))
+        [edit-mode, set-edit-mode!] (rum/use-state nil) ;; code
         update-setting! (fn [k v] (.set plugin-settings (name k) (bean/->js v)))]
 
     (rum/use-effect!
      (fn []
        (let [on-change (fn [^js s]
                          (when-let [s (bean/->clj s)]
-                           (set-settings s)))]
+                           (set-settings! s)))]
          (.on plugin-settings "change" on-change)
          #(.off plugin-settings "change" on-change)))
      [pid])
@@ -123,27 +133,56 @@
       [:<>
        [:h2.text-xl.px-2.pt-1.opacity-90 "ID: " pid]
        [:div.cp__plugins-settings-inner
+        {:data-mode (some-> edit-mode (name))}
         ;; settings.json
         [:span.edit-file
-         (edit-settings-file pid nil)]
-
-        ;; render items
-        (for [desc schema
-              :let [key (:key desc)
-                    val (get settings (keyword key))
-                    type (keyword (:type desc))
-                    desc (update desc :description #(plugin-handler/markdown-to-html %))]]
-
-          (rum/with-key
-            (condp contains? type
-              #{:string :number} (render-item-input val desc update-setting!)
-              #{:boolean} (render-item-toggle val desc update-setting!)
-              #{:enum} (render-item-enum val desc update-setting!)
-              #{:object} (render-item-object val desc pid)
-              #{:heading} (render-item-heading desc)
-
-              (render-item-not-handled key))
-            key))]]
+         (edit-settings-file pid {:set-edit-mode! set-edit-mode!
+                                  :edit-mode edit-mode})]
+
+        (if (= edit-mode :code)
+          ;; render with code editor
+          [:div.code-mode-wrap.pl-3.pr-1.py-1.mb-8.-ml-1
+           (let [content' (js/JSON.stringify (bean/->js settings) nil 2)]
+             (lazy-editor/editor {:file? false}
+               (str "code-edit-lsp-settings")
+               {:data-lang "json"}
+               content' {}))
+           [:div.flex.justify-end.pt-2.gap-2
+            (shui/button {:size :sm :variant :ghost
+                          :on-click (fn [^js e]
+                                      (let [^js cm (util/get-cm-instance (-> (.-target e) (.closest ".code-mode-wrap")))
+                                            content' (some-> (.toJSON plugin-settings) (js/JSON.stringify nil 2))]
+                                        (.setValue cm content')))}
+              "Reset")
+            (shui/button {:size :sm
+                          :on-click (fn [^js e]
+                                      (try
+                                        (let [^js cm (util/get-cm-instance (-> (.-target e) (.closest ".code-mode-wrap")))
+                                              content (.getValue cm)
+                                              content' (js/JSON.parse content)]
+                                          (set! (. plugin-settings -settings) content')
+                                          (set-edit-mode! nil))
+                                        (catch js/Error e
+                                          (notification/show! (.-message e) :error))))}
+              "Save")]]
+
+          ;; render with gui items
+          (for [desc schema
+                :let [key (:key desc)
+                      val (get settings (keyword key))
+                      type (keyword (:type desc))
+                      desc (update desc :description #(plugin-handler/markdown-to-html %))]]
+
+            (rum/with-key
+              (condp contains? type
+                #{:string :number} (render-item-input val desc update-setting!)
+                #{:boolean} (render-item-toggle val desc update-setting!)
+                #{:enum} (render-item-enum val desc update-setting!)
+                #{:object} (render-item-object val desc pid)
+                #{:heading} (render-item-heading desc)
+
+                (render-item-not-handled key))
+              key)))]]
 
       ;; no settings
       [:h2.font-bold.text-lg.py-4.warning "No Settings Schema!"])))

+ 7 - 5
src/main/frontend/components/settings.cljs

@@ -640,10 +640,12 @@
                      (storage/set ::storage-spec/lsp-core-enabled v))]
     [:div.flex.items-center.gap-2
      (ui/toggle on? on-toggle true)
-     (when (not= (boolean value) on?)
-       (ui/button (t :plugin/restart)
-                  :on-click #(js/logseq.api.relaunch)
-                  :small? true :intent "logseq"))]))
+
+     (when (util/electron?)
+       (when (not= (boolean value) on?)
+         (ui/button (t :plugin/restart)
+           :on-click #(js/logseq.api.relaunch)
+           :small? true :intent "logseq")))]))
 
 (rum/defc http-server-enabled-switcher
   [t]
@@ -1112,7 +1114,7 @@
                              (when (= "Enter" (util/ekey e))
                                (update-home-page e)))}]]]])
      (when-not db-based? (whiteboards-switcher-row enable-whiteboards?))
-     (when (and (util/electron?) config/feature-plugin-system-on?)
+     (when (and web-platform? config/feature-plugin-system-on?)
        (plugin-system-switcher-row))
      (when (util/electron?)
        (http-server-switcher-row))

+ 0 - 11
src/main/frontend/components/svg.cljs

@@ -300,17 +300,6 @@
     [:path {:stroke "none" :d "M0 0h24v24H0z" :fill "none"}]
     [:path {:d "M12 17.75l-6.172 3.245l1.179 -6.873l-5 -4.867l6.9 -1l3.086 -6.253l3.086 6.253l6.9 1l-5 4.867l1.179 6.873z"}]]))
 
-(defn apps
-  ([] (apps 16))
-  ([size]
-   [:svg.icon-apps {:width size :height size :viewBox "0 0 24 24" :stroke-width "2" :stroke "currentColor" :fill "none" :stroke-linecap "round" :stroke-linejoin "round"}
-    [:path {:stroke "none" :d "M0 0h24v24H0z" :fill "none"}]
-    [:rect {:x "4" :y "4" :width "6" :height "6" :rx "1"}]
-    [:rect {:x "4" :y "14" :width "6" :height "6" :rx "1"}]
-    [:rect {:x "14" :y "14" :width "6" :height "6" :rx "1"}]
-    [:line {:x1 "14" :y1 "7" :x2 "20" :y2 "7"}]
-    [:line {:x1 "17" :y1 "4" :x2 "17" :y2 "10"}]]))
-
 (defn reload
   ([] (reload 16))
   ([size]

+ 3 - 4
src/main/frontend/components/theme.cljs

@@ -85,11 +85,10 @@
 
     (rum/use-effect!
      #(when config/lsp-enabled?
-        (plugin-handler/setup-install-listener!)
-        (plugin-config-handler/setup-install-listener!)
         (plugin-handler/load-plugin-preferences)
-        (fn []
-          (js/window.apis.removeAllListeners (name :lsp-updates))))
+        (comp
+          (plugin-handler/setup-install-listener!)
+          (plugin-config-handler/setup-install-listener!)))
      [])
 
     (rum/use-effect!

+ 3 - 3
src/main/frontend/config.cljs

@@ -74,9 +74,9 @@
 
 ;; User level configuration for whether plugins are enabled
 (defonce lsp-enabled?
-  (and (util/electron?)
-       (not (false? feature-plugin-system-on?))
-       (state/lsp-enabled?-or-theme)))
+  (and util/plugin-platform?
+    (not (false? feature-plugin-system-on?))
+    (state/lsp-enabled?-or-theme)))
 
 (defn plugin-config-enabled?
   []

+ 2 - 1
src/main/frontend/handler.cljs

@@ -136,6 +136,8 @@
 
 (defn start!
   [render]
+
+  (idb/start)
   (test/setup-test!)
   (get-system-info)
   (set-global-error-notification!)
@@ -156,7 +158,6 @@
       (p/catch (fn [_e]
                  (notification/show! "Sorry, it seems that your browser doesn't support IndexedDB, we recommend to use latest Chrome(Chromium) or Firefox(Non-private mode)." :error false)
                  (state/set-indexedb-support! false))))
-  (idb/start)
 
   (react/run-custom-queries-when-idle!)
 

+ 2 - 2
src/main/frontend/handler/command_palette.cljs

@@ -11,14 +11,14 @@
 (s/def :command/id keyword?)
 (s/def :command/desc string?)
 (s/def :command/action fn?)
-(s/def :command/shortcut string?)
+(s/def :command/shortcut (s/or :nil nil? :keybinding string?))
 (s/def :command/tag vector?)
 
 (s/def :command/command
   (s/keys :req-un [:command/id :command/action]
           ;; :command/desc is optional for internal commands since view
           ;; checks translation ns first
-          :opt-un [:command/desc :command/shortcut :command/tag]))
+          :opt-un [:command/desc :command/shortcut :command/tag :command/handler-id]))
 
 (defn global-shortcut-commands []
   (->> [:shortcut.handler/editor-global

+ 60 - 6
src/main/frontend/handler/common/plugin.cljs

@@ -1,24 +1,78 @@
 (ns frontend.handler.common.plugin
   "Common plugin related fns for handlers and api"
   (:require [frontend.state :as state]
+            [frontend.util :as util]
             [promesa.core :as p]
+            [cljs-bean.core :as bean]
             [electron.ipc :as ipc]))
 
+(defn get-web-plugin-checker-url!
+  ([repo] (get-web-plugin-checker-url! repo ""))
+  ([repo version]
+   (util/node-path.join "https://plugins.logseq.io/r2"
+     repo (if (not (string? version)) "" version))))
+
+(defn fetch-web-plugin-entry-info
+  [repo version]
+  (p/let [url (get-web-plugin-checker-url! repo version)
+          ^js res (js/window.fetch url)]
+    (if (and (.-ok res)
+          (= (.-status res) 200))
+      (-> (.json res)
+        (p/then #(bean/->clj %)))
+      (-> (.text res)
+        (p/then
+          (fn [error-text]
+            (throw (js/Error. (str "web-plugin-entry-error:" error-text)))))))))
+
 (defn installed?
   "For the given plugin id, returns boolean indicating if it is installed"
   [id]
-  (and (contains? (:plugin/installed-plugins @state/state) (keyword id))
-       (get-in @state/state [:plugin/installed-plugins (keyword id) :iir])))
+  (contains? (:plugin/installed-plugins @state/state) (keyword id)))
+
+(defn emit-lsp-updates!
+  [payload]
+  (js/console.log "debug:lsp-updates:" payload)
+  (js/window.apis.emit (name :lsp-updates) (bean/->js payload)))
+
+(defn async-install-or-update-for-web!
+  [{:keys [version repo only-check] :as manifest}]
+  (js/console.log "[plugin]" (if only-check "Checking" "Installing") " #" repo)
+  (-> (fetch-web-plugin-entry-info repo (if only-check "" version))
+    (p/then (fn [web-pkg]
+             (let [web-pkg (merge web-pkg (dissoc manifest :stat))
+                   latest-version (:version web-pkg)
+                   valid-latest-version (when only-check
+                                          (let [coerced-current-version (.coerce util/sem-ver version)
+                                                coerced-latest-version (.coerce util/sem-ver latest-version)]
+                                            (if (and coerced-current-version
+                                                  coerced-latest-version
+                                                  (util/sem-ver.lt coerced-current-version coerced-latest-version))
+                                              latest-version
+                                              (throw (js/Error. :no-new-version)))))]
+              (emit-lsp-updates!
+                {:status :completed
+                 :only-check only-check
+                 :payload (if only-check
+                            (assoc manifest :latest-version valid-latest-version  :latest-notes "TODO: update notes")
+                            (assoc manifest :dst repo :version latest-version :web-pkg web-pkg))}))))
+    (p/catch (fn [^js e]
+               (emit-lsp-updates!
+                 {:status :error
+                  :only-check only-check
+                  :payload (assoc manifest :error-code (.-message e))})))))
 
-(defn install-marketplace-plugin
+(defn install-marketplace-plugin!
   "Installs plugin given plugin map with id"
-  [{:keys [id] :as mft}]
+  [{:keys [id] :as manifest}]
   (when-not (and (:plugin/installing @state/state)
                  (installed? id))
     (p/create
      (fn [resolve]
-       (state/set-state! :plugin/installing mft)
-       (ipc/ipc :installMarketPlugin mft)
+       (state/set-state! :plugin/installing manifest)
+       (if (util/electron?)
+         (ipc/ipc :installMarketPlugin manifest)
+         (async-install-or-update-for-web! manifest))
        (resolve id)))))
 
 (defn unregister-plugin

+ 3 - 2
src/main/frontend/handler/config.cljs

@@ -1,6 +1,7 @@
 (ns frontend.handler.config
   "Fns for setting repo config"
-  (:require [frontend.state :as state]
+  (:require [clojure.string :as string]
+            [frontend.state :as state]
             [frontend.handler.file :as file-handler]
             [frontend.handler.repo-config :as repo-config-handler]
             [frontend.handler.db-based.editor :as db-editor-handler]
@@ -18,7 +19,7 @@
   (when-let [repo (state/get-current-repo)]
     (when-let [content (db/get-file path)]
       (repo-config-handler/read-repo-config content)
-      (let [result (parse-repo-config content)
+      (let [result (parse-repo-config (if (string/blank? content) "{}" content))
             ks (if (vector? k) k [k])
             v (cond->> v
                        (map? v)

+ 199 - 127
src/main/frontend/handler/plugin.cljs

@@ -13,6 +13,7 @@
             [frontend.state :as state]
             [medley.core :as medley]
             [frontend.fs :as fs]
+            [frontend.idb :as idb]
             [electron.ipc :as ipc]
             [cljs-bean.core :as bean]
             [clojure.string :as string]
@@ -37,7 +38,7 @@
 (defn invoke-exported-api
   [type & args]
   (try
-    (apply js-invoke (aget js/window.logseq "api") type args)
+    (apply js-invoke (aget js/window.logseq "api") (name type) args)
     (catch :default e (js/console.error e))))
 
 (defn markdown-to-html
@@ -54,9 +55,21 @@
 (defonce stats-url (str central-endpoint "stats.json"))
 (declare select-a-plugin-theme)
 
+(defn setup-global-apis-for-web!
+  []
+  (when (and util/web-platform?
+          (nil? js/window.apis))
+    (let [^js e (js/window.EventEmitter3.)]
+      (set! (. js/window -apis) e))))
+
+(defn unlink-plugin-for-web!
+  [key]
+  (invoke-exported-api :unlink_installed_web_plugin key)
+  (invoke-exported-api :unlink_plugin_user_settings key))
+
 (defn load-plugin-preferences
   []
-  (-> (invoke-exported-api "load_user_preferences")
+  (-> (invoke-exported-api :load_user_preferences)
     (p/then #(bean/->clj %))
     (p/then #(state/set-state! :plugin/preferences %))
     (p/catch
@@ -86,7 +99,9 @@
       (fn [resolve reject]
         (let [on-ok (fn [res]
                       (if-let [res (and res (bean/->clj res))]
-                        (let [pkgs (:packages res)]
+                        (let [pkgs (:packages res)
+                              pkgs (if (util/electron?) pkgs
+                                     (some->> pkgs (filterv #(not (true? (:effect %))))))]
                           (state/set-state! :plugin/marketplace-pkgs pkgs)
                           (resolve pkgs))
                         (reject nil)))]
@@ -128,10 +143,13 @@
     (state/set-state! :plugin/installing pkg)
 
     (-> (load-marketplace-plugins false)
-      (p/then (fn [mfts]
-                (let [mft (some #(when (= (:id %) id) %) mfts)]
+      (p/then (fn [manifests]
+                (let [mft (some #(when (= (:id %) id) %) manifests)
+                      opts (merge (dissoc pkg :logger) mft)]
                   ;;TODO: (throw (js/Error. [:not-found-in-marketplace id]))
-                  (ipc/ipc :updateMarketPlugin (merge (dissoc pkg :logger) mft)))
+                  (if (util/electron?)
+                    (ipc/ipc :updateMarketPlugin opts)
+                    (plugin-common-handler/async-install-or-update-for-web! opts)))
                 true))
       (p/catch (fn [^js e]
                  (state/reset-all-updates-state)
@@ -189,68 +207,82 @@
 (defn setup-install-listener!
   []
   (let [channel (name :lsp-updates)
-        listener (fn [_ ^js e]
-                   (when-let [{:keys [status payload only-check]} (bean/->clj e)]
-                     (case (keyword status)
-
-                       :completed
-                       (let [{:keys [id dst name title theme]} payload
-                             name (or title name "Untitled")]
-                         (if only-check
-                           (state/consume-updates-from-coming-plugin! payload false)
-                           (if (plugin-common-handler/installed? id)
-                             (when-let [^js pl (get-plugin-inst id)] ;; update
-                               (p/then
-                                 (.reload pl)
-                                 #(do
-                                    ;;(if theme (select-a-plugin-theme id))
-                                    (notification/show!
-                                      (t :plugin/update-plugin name (.-version (.-options pl))) :success)
-                                    (state/consume-updates-from-coming-plugin! payload true))))
-
-                             (do                            ;; register new
-                               (p/then
-                                 (js/LSPluginCore.register (bean/->js {:key id :url dst}))
-                                 (fn [] (when theme (js/setTimeout #(select-a-plugin-theme id) 300))))
+        listener (fn [ctx ^js evt]
+                   (let [e (or evt ctx)]
+                     (when-let [{:keys [status payload only-check]} (bean/->clj e)]
+                       (case (keyword status)
+
+                         :completed
+                         (let [{:keys [id dst name title theme web-pkg]} payload
+                               name (or title name "Untitled")]
+                           (if only-check
+                             (state/consume-updates-from-coming-plugin! payload false)
+                             (if (plugin-common-handler/installed? id)
+                               ;; update plugin
+                               (when-let [^js pl (get-plugin-inst id)]
+                                 (p/then
+                                   (.reload pl)
+                                   #(do
+                                      ;;(if theme (select-a-plugin-theme id))
+                                      (when (not (util/electron?))
+                                        (set! (.-version (.-options pl)) (:version web-pkg))
+                                        (set! (.-webPkg (.-options pl)) (bean/->js web-pkg))
+                                        (invoke-exported-api :save_installed_web_plugin (.toJSON pl false)))
+                                      (notification/show!
+                                        (t :plugin/update-plugin name (.-version (.-options pl))) :success)
+                                      (state/consume-updates-from-coming-plugin! payload true))))
+                               ;; register plugin
+                               (-> (js/LSPluginCore.register (bean/->js {:key id :url dst :webPkg web-pkg}))
+                                 (p/then (fn []
+                                           (when-let [^js pl (get-plugin-inst id)]
+                                             (when theme (js/setTimeout #(select-a-plugin-theme id) 300))
+                                             (when (.-isWebPlugin pl)
+                                               (invoke-exported-api :save_installed_web_plugin (.toJSON pl false)))
+                                             (notification/show!
+                                               (t :plugin/installed-plugin name) :success))))
+                                 (p/catch (fn [^js e]
+                                            (notification/show!
+                                              (str "Install failed: " name "\n" (.-message e))
+                                              :error)))))))
+
+                         :error
+                         (let [error-code (keyword (string/replace (:error-code payload) #"^[\s\:\[]+" ""))
+                               fake-error? (contains? #{:no-new-version} error-code)
+                               [msg type] (case error-code
+
+                                            :no-new-version
+                                            [(t :plugin/up-to-date ":)") :success]
+
+                                            [error-code :error])
+                               pending? (seq (:plugin/updates-pending @state/state))]
+
+                           (if (and only-check pending?)
+                             (state/consume-updates-from-coming-plugin! payload false)
+
+                             (do
+                               ;; consume failed download updates
+                               (when (and (not only-check) (not pending?))
+                                 (state/consume-updates-from-coming-plugin! payload true))
+
+                               ;; notify human tips
                                (notification/show!
-                                 (t :plugin/installed-plugin name) :success)))))
-
-                       :error
-                       (let [error-code (keyword (string/replace (:error-code payload) #"^[\s\:\[]+" ""))
-                             fake-error? (contains? #{:no-new-version} error-code)
-                             [msg type] (case error-code
-
-                                          :no-new-version
-                                          [(t :plugin/up-to-date ":)") :success]
-
-                                          [error-code :error])
-                             pending? (seq (:plugin/updates-pending @state/state))]
-
-                         (if (and only-check pending?)
-                           (state/consume-updates-from-coming-plugin! payload false)
-
-                           (do
-                             ;; consume failed download updates
-                             (when (and (not only-check) (not pending?))
-                               (state/consume-updates-from-coming-plugin! payload true))
+                                 (str
+                                   (if (= :error type) "[Error]" "")
+                                   (str "<" (:id payload) "> ")
+                                   msg) type)))
 
-                             ;; notify human tips
-                             (notification/show!
-                               (str
-                                 (if (= :error type) "[Error]" "")
-                                 (str "<" (:id payload) "> ")
-                                 msg) type)))
+                           (when-not fake-error?
+                             (js/console.error "Update Error:" (:error-code payload))))
 
-                         (when-not fake-error?
-                           (js/console.error "Update Error:" (:error-code payload))))
-
-                       :default))
+                         :default)))
 
                    ;; reset
                    (js/setTimeout #(state/set-state! :plugin/installing nil) 512)
                    true)]
-
-    (js/window.apis.addListener channel listener)))
+    (js/window.apis.addListener channel listener)
+    ;; teardown
+    (fn []
+      (js/window.apis.removeListener channel listener))))
 
 (defn- normalize-plugin-metadata
   [metadata]
@@ -526,7 +558,7 @@
         (p/catch #(do (js/console.warn %)
                       (notification/show! "No README content." :warning))))
       ;; market
-      (shui/dialog-open! (fn [_] (display repo nil)) {:label "plugin-readme"}))))
+      (shui/dialog-open! (fn [_] (display item nil)) {:label "plugin-readme"}))))
 
 (defn load-unpacked-plugin
   []
@@ -579,40 +611,54 @@
   (when-let [type (and block (str "slot:" (:block/uuid block)))]
     (hook-plugin-editor type (merge payload block) nil)))
 
-(defn get-ls-dotdir-root
+(defonce *ls-dotdir-root (atom nil))
+(defn get-ls-dotdir-root [] @*ls-dotdir-root)
+
+(defn init-ls-dotdir-root
   []
-  (ipc/ipc "getLogseqDotDirRoot"))
+  (-> (if (util/electron?)
+        (ipc/ipc "getLogseqDotDirRoot")
+        "LSPUserDotRoot/")
+    (p/then #(do (reset! *ls-dotdir-root %) %))))
+
 
 (defn make-fn-to-load-dotdir-json
-  [dirname default]
+  [dirname ^js default]
   (fn [key]
     (when-let [key (and key (name key))]
-      (p/let [repo ""
-              path (get-ls-dotdir-root)
-              exist? (fs/file-exists? path dirname)
-              _ (when-not exist? (fs/mkdir! (util/node-path.join path dirname)))
-              path (util/node-path.join path dirname (str key ".json"))
-              _ (fs/create-if-not-exists repo nil path (or default "{}"))
-              json (fs/read-file nil path)]
-        [path (js/JSON.parse json)]))))
+      (let [repo ""
+            path (get-ls-dotdir-root)
+            path (util/node-path.join path dirname (str key ".json"))]
+        (if (util/electron?)
+          (p/let [exist? (fs/file-exists? path dirname)
+                  _ (when-not exist? (fs/mkdir! (util/node-path.join path dirname)))
+                  _ (fs/create-if-not-exists repo nil path (js/JSON.stringify default))
+                  json (fs/read-file nil path)]
+            [path (js/JSON.parse json)])
+          (p/let [data (idb/get-item path)]
+            [path (or data default)]))))))
 
 (defn make-fn-to-save-dotdir-json
   [dirname]
-  (fn [key content]
+  (fn [key ^js data]
     (when-let [key (and key (name key))]
-      (p/let [repo ""
-              path (get-ls-dotdir-root)
-              path (util/node-path.join path dirname (str key ".json"))]
-        (fs/write-file! repo nil path content {:skip-compare? true})))))
+      (let [repo ""
+            path (get-ls-dotdir-root)
+            path (util/node-path.join path dirname (str key ".json"))]
+        (if (util/electron?)
+          (fs/write-file! repo nil path (js/JSON.stringify data nil 2) {:skip-compare? true})
+          (idb/set-item! path data))))))
 
 (defn make-fn-to-unlink-dotdir-json
   [dirname]
   (fn [key]
     (when-let [key (and key (name key))]
-      (p/let [repo ""
-              path (get-ls-dotdir-root)
-              path (util/node-path.join path dirname (str key ".json"))]
-        (fs/unlink! repo path nil)))))
+      (let [repo ""
+            path (get-ls-dotdir-root)
+            path (util/node-path.join path dirname (str key ".json"))]
+        (if (util/electron?)
+          (fs/unlink! repo path nil)
+          (idb/remove-item! path))))))
 
 (defn show-themes-modal!
   ([] (show-themes-modal! false))
@@ -629,12 +675,17 @@
 
 (defn- get-user-default-plugins
   []
-  (p/catch
-   (p/let [files ^js (ipc/ipc "getUserDefaultPlugins")
-           files (js->clj files)]
-     (map #(hash-map :url %) files))
-   (fn [e]
-     (js/console.error e))))
+  (-> (if (util/electron?)
+        (ipc/ipc "getUserDefaultPlugins")
+        (invoke-exported-api :load_installed_web_plugins))
+      (p/then #(bean/->clj %))
+      (p/then (fn [plugins]
+                (if (util/electron?)
+                  (map #(hash-map :url %) plugins)
+                  (some->> (vals plugins)
+                           (filter #(:url %))))))
+      (p/catch (fn [e]
+                 (js/console.error "[get-user-default-plugins:error]" e)))))
 
 (defn set-auto-checking!
   [v]
@@ -728,11 +779,11 @@
 (rum/defc lsp-indicator < rum/reactive
   []
   (let [text (state/sub :plugin/indicator-text)]
-    (when-not (= text "END")
-      [:div.flex.align-items.justify-center.h-screen.w-full.preboot-loading
-       [:span.flex.items-center.justify-center.w-60.flex-col
-        [:small.scale-250.opacity-70.mb-10.animate-pulse (svg/logo)]
-        [:small.block.text-sm.relative.opacity-50 {:style {:right "-8px"}} text]]])))
+    [:div.flex.align-items.justify-center.h-screen.w-full.preboot-loading
+     [:span.flex.items-center.justify-center.flex-col
+      [:small.scale-250.opacity-50.mb-10.animate-pulse (svg/logo)]
+      [:small.block.text-sm.relative.opacity-50 {:style {:right "-8px" :min-height "24px"}}
+       (str text)]]]))
 
 (defn ^:large-vars/cleanup-todo init-plugins!
   [callback]
@@ -742,9 +793,7 @@
     (rum/mount
       (lsp-indicator) el))
 
-  (state/set-state! :plugin/indicator-text "LOADING")
-
-  (-> (p/let [root (get-ls-dotdir-root)
+  (-> (p/let [root (init-ls-dotdir-root)
               _ (.setupPluginCore js/LSPlugin (bean/->js {:localUserConfigRoot root :dotConfigRoot root}))
 
               clear-commands! (fn [pid]
@@ -758,14 +807,20 @@
 
               _ (doto js/LSPluginCore
                   (.on "registered"
-                    (fn [^js pl]
-                      (register-plugin
-                        (bean/->clj (.parse js/JSON (.stringify js/JSON pl))))))
+                       (fn [^js pl]
+                         (register-plugin
+                           (bean/->clj (.parse js/JSON (.stringify js/JSON pl))))))
+
+                  (.on "beforeload"
+                       (fn [^js pl]
+                         (let [text (if (util/electron?)
+                                      (util/format "Load plugin: %s..." (.-id pl)) "Loading")]
+                           (state/set-state! :plugin/indicator-text text))))
 
                   (.on "reloaded"
-                    (fn [^js pl]
-                      (register-plugin
-                        (bean/->clj (.parse js/JSON (.stringify js/JSON pl))))))
+                       (fn [^js pl]
+                         (register-plugin
+                           (bean/->clj (.parse js/JSON (.stringify js/JSON pl))))))
 
                   (.on "unregistered" (fn [pid]
                                         (let [pid (keyword pid)]
@@ -778,7 +833,9 @@
 
                   (.on "unlink-plugin" (fn [pid]
                                          (let [pid (keyword pid)]
-                                           (ipc/ipc "uninstallMarketPlugin" (name pid)))))
+                                           (if (util/electron?)
+                                             (ipc/ipc :uninstallMarketPlugin (name pid))
+                                             (unlink-plugin-for-web! pid)))))
 
                   (.on "beforereload" (fn [^js pl]
                                         (let [pid (.-id pl)]
@@ -791,12 +848,13 @@
 
                   (.on "themes-changed" (fn [^js themes]
                                           (swap! state/state assoc :plugin/installed-themes
-                                            (vec (mapcat (fn [[pid vs]] (mapv #(assoc % :pid pid) (bean/->clj vs))) (bean/->clj themes))))))
+                                                 (vec (mapcat (fn [[pid vs]] (mapv #(assoc % :pid pid) (bean/->clj vs))) (bean/->clj themes))))))
 
-                  (.on "theme-selected" (fn [^js theme]
+                  (.on "theme-selected" (fn [^js theme ^js opts]
                                           (let [theme (bean/->clj theme)
+                                                _opts (bean/->clj opts)
                                                 url (:url theme)
-                                                mode (:mode theme)]
+                                                mode (or (:mode theme) (state/sub :ui/theme))]
                                             (when mode
                                               (state/set-custom-theme! mode theme)
                                               (state/set-theme-mode! mode))
@@ -808,41 +866,52 @@
                                                     custom-theme (dissoc themes :mode)
                                                     mode (:mode themes)]
                                                 (state/set-custom-theme! {:light (if (nil? (:light custom-theme)) {:mode "light"} (:light custom-theme))
-                                                                          :dark (if (nil? (:dark custom-theme)) {:mode "dark"} (:dark custom-theme))})
+                                                                          :dark  (if (nil? (:dark custom-theme)) {:mode "dark"} (:dark custom-theme))})
                                                 (state/set-theme-mode! mode))))
 
                   (.on "settings-changed" (fn [id ^js settings]
                                             (let [id (keyword id)]
                                               (when (and settings
-                                                      (contains? (:plugin/installed-plugins @state/state) id))
+                                                         (contains? (:plugin/installed-plugins @state/state) id))
                                                 (update-plugin-settings-state id (bean/->clj settings))))))
 
                   (.on "ready" (fn [^js perf-table]
                                  (when-let [plugins (and perf-table (.entries perf-table))]
                                    (->> plugins
-                                     (keep
-                                       (fn [[_k ^js v]]
-                                         (when-let [end (and (some-> v (.-o) (.-disabled) (not))
-                                                          (.-e v))]
-                                           (when (and (number? end)
-                                                   ;; valid end time
-                                                   (> end 0)
-                                                   ;; greater than 6s
-                                                   (> (- end (.-s v)) 6000))
-                                             v))))
-                                     ((fn [perfs]
-                                        (doseq [perf perfs]
-                                          (state/pub-event! [:plugin/loader-perf-tip (bean/->clj perf)])))))))))
+                                        (keep
+                                          (fn [[_k ^js v]]
+                                            (when-let [end (and (some-> v (.-o) (.-disabled) (not))
+                                                                (.-e v))]
+                                              (when (and (number? end)
+                                                         ;; valid end time
+                                                         (> end 0)
+                                                         ;; greater than 6s
+                                                         (> (- end (.-s v)) 6000))
+                                                v))))
+                                        ((fn [perfs]
+                                           (doseq [perf perfs]
+                                             (state/pub-event! [:plugin/loader-perf-tip (bean/->clj perf)])))))))))
 
               default-plugins (get-user-default-plugins)
-
-              _ (.register js/LSPluginCore (bean/->js (if (seq default-plugins) default-plugins [])) true)])
+              [plugins0, plugins-async] (if (and (seq default-plugins)
+                                         (not (util/electron?)))
+                                     ((juxt (fn [its] (filterv #(:theme %) its))
+                                        (fn [its] (filterv #(not (:theme %)) its)))
+                                      default-plugins)
+                                     [default-plugins])
+              _ (.register js/LSPluginCore (bean/->js (if (seq plugins0) plugins0 [])) true)]
+        plugins-async)
 
     (p/then
-      (fn []
-        (state/set-state! :plugin/indicator-text "END")
+      (fn [plugins-async]
+        (state/set-state! :plugin/indicator-text nil)
         ;; wait for the plugin register async messages
-        (js/setTimeout #(callback) 64)))
+        (js/setTimeout
+          (fn [] (callback)
+            (some-> (seq plugins-async)
+              (p/delay 16)
+              (p/then #(.register js/LSPluginCore (bean/->js plugins-async) true))))
+          (if (util/electron?) 64 0))))
     (p/catch
      (fn [^js e]
        (log/error :setup-plugin-system-error e)
@@ -853,7 +922,10 @@
   [callback]
   (if (not config/lsp-enabled?)
     (callback)
-    (init-plugins! callback)))
+    (do
+      (idb/start)
+      (setup-global-apis-for-web!)
+      (init-plugins! callback))))
 
 (comment
   {:pending (count (:plugin/updates-pending @state/state))

+ 16 - 9
src/main/frontend/handler/plugin_config.cljs

@@ -11,6 +11,7 @@ when a plugin is installed, updated or removed"
             [frontend.state :as state]
             [frontend.handler.notification :as notification]
             [frontend.handler.common.plugin :as plugin-common-handler]
+            [frontend.util :as util]
             [clojure.edn :as edn]
             [clojure.set :as set]
             [clojure.pprint :as pprint]
@@ -103,25 +104,31 @@ returns map of plugins to install and uninstall"
     (plugin-common-handler/unregister-plugin (name (:id plugin))))
   (log/info :install-plugins (:install plugins))
   (doseq [plugin (:install plugins)]
-    (plugin-common-handler/install-marketplace-plugin
+    (plugin-common-handler/install-marketplace-plugin!
      ;; Add :name so that install notifications are readable
      (assoc plugin :name (name (:id plugin))))))
 
 (defn setup-install-listener!
   "Sets up a listener for the lsp-installed event to update plugins.edn"
   []
-  (let [listener (fn listener [_ e]
+  (let [channel (name :lsp-updates)
+        listener (fn listener [_ e]
                    (when-let [{:keys [status payload only-check]} (bean/->clj e)]
                      (when (and (= status "completed") (not only-check))
                        (let [{:keys [theme effect]} payload]
                          (add-or-update-plugin
-                          (assoc payload
-                                 :version (:installed-version payload)
-                                 :effect (boolean effect)
-                                 ;; Manual installation doesn't have theme field but
-                                 ;; plugin.edn requires this field
-                                 :theme (boolean theme)))))))]
-    (js/window.apis.addListener (name :lsp-updates) listener)))
+                           (assoc payload
+                             :version (:installed-version payload)
+                             :effect (boolean effect)
+                             ;; Manual installation doesn't have theme field but
+                             ;; plugin.edn requires this field
+                             :theme (boolean theme)))))))]
+    (when (util/electron?)
+      (js/window.apis.addListener channel listener))
+    ;;teardown
+    (fn []
+      (when (util/electron?)
+        (js/window.apis.removeListener channel listener)))))
 
 (defn start
   "This component has just one responsibility on start, to create a plugins.edn

+ 2 - 1
src/main/frontend/idb.cljs

@@ -64,4 +64,5 @@
 (defn start
   "This component's only responsibility is to create a Store object"
   []
-  (reset! store (idb-keyval/newStore "localforage" "keyvaluepairs" 2)))
+  (when (nil? @store)
+    (reset! store (idb-keyval/newStore "localforage" "keyvaluepairs" 2))))

+ 3 - 3
src/main/frontend/modules/shortcut/core.cljs

@@ -288,6 +288,6 @@
                          (boolean? binding)))
                 (assoc id binding)))]
       ;; TODO: exclude current graph config shortcuts
-      (when (nil? binding)
-        (config-handler/set-config! :shortcuts (into-shortcuts graph-shortcuts)))
-      (global-config-handler/set-global-config-kv! :shortcuts (into-shortcuts global-shortcuts)))))
+      (config-handler/set-config! :shortcuts (into-shortcuts graph-shortcuts))
+      (when (util/electron?)
+        (global-config-handler/set-global-config-kv! :shortcuts (into-shortcuts global-shortcuts))))))

+ 2 - 2
src/main/frontend/state.cljs

@@ -87,7 +87,7 @@
       :ui/settings-open?                     false
       :ui/sidebar-open?                      false
       :ui/sidebar-width                      "40%"
-      :ui/left-sidebar-open?                 (boolean (storage/get "ls-left-sidebar-open?"))
+      :ui/left-sidebar-open?                 (boolean (storage/get :ls-left-sidebar-open?))
       :ui/theme                              (or (storage/get :ui/theme) "light")
       :ui/system-theme?                      ((fnil identity (or util/mac? util/win32? false)) (storage/get :ui/system-theme?))
       :ui/custom-theme                       (or (storage/get :ui/custom-theme) {:light {:mode "light"} :dark {:mode "dark"}})
@@ -204,7 +204,7 @@
       :mobile/app-state-change                 (atom nil)
 
       ;; plugin
-      :plugin/enabled                        (and (util/electron?)
+      :plugin/enabled                        (and util/plugin-platform?
                                                   ;; true false :theme-only
                                                   ((fnil identity true) (storage/get ::storage-spec/lsp-core-enabled)))
       :plugin/preferences                    nil

+ 4 - 1
src/main/frontend/util.cljc

@@ -11,6 +11,7 @@
             ["sanitize-filename" :as sanitizeFilename]
             ["check-password-strength" :refer [passwordStrength]]
             ["path-complete-extname" :as pathCompleteExtname]
+            ["semver" :as semver]
             [frontend.loader :refer [load]]
             [cljs-bean.core :as bean]
             [cljs-time.coerce :as tc]
@@ -50,6 +51,7 @@
      (-namespace [_] nil)))
 
 #?(:cljs (defonce ^js node-path utils/nodePath))
+#?(:cljs (defonce ^js sem-ver semver))
 #?(:cljs (defonce ^js full-path-extname pathCompleteExtname))
 #?(:cljs (defn app-scroll-container-node
            ([]
@@ -146,7 +148,8 @@
    (do
      (def nfs? (and (not (electron?))
                     (not (mobile-util/native-platform?))))
-     (def web-platform? nfs?)))
+     (def web-platform? nfs?)
+     (def plugin-platform? (or web-platform? (electron?)))))
 
 #?(:cljs
    (defn file-protocol?

+ 0 - 1
src/main/frontend/utils.js

@@ -2,7 +2,6 @@ import path from 'path/path.js'
 
 // TODO split the capacitor abilities to a separate file for capacitor APIs
 import { Capacitor } from '@capacitor/core'
-import { StatusBar, Style } from '@capacitor/status-bar'
 import { Clipboard as CapacitorClipboard } from '@capacitor/clipboard'
 
 if (typeof window === 'undefined') {

+ 48 - 18
src/main/logseq/api.cljs

@@ -19,6 +19,7 @@
             [frontend.handler.recent :as recent-handler]
             [frontend.handler.route :as route-handler]
             [frontend.db :as db]
+            [frontend.idb :as idb]
             [frontend.db.async :as db-async]
             [frontend.db.model :as db-model]
             [frontend.db.query-custom :as query-custom]
@@ -205,16 +206,18 @@
 
 (def ^:export load_plugin_config
   (fn [path]
-    (fs/read-file nil (util/node-path.join path "package.json"))))
+    (if (util/electron?)
+      (fs/read-file nil (util/node-path.join path "package.json"))
+      (do (js/console.log "==>>> TODO: load plugin package.json from local???")
+        ""))))
 
 (def ^:export load_plugin_readme
   (fn [path]
     (fs/read-file nil (util/node-path.join path "readme.md"))))
 
-(def ^:export save_plugin_config
+(def ^:export save_plugin_package_json
   (fn [path ^js data]
     (let [repo ""
-
           path (util/node-path.join path "package.json")]
       (fs/write-file! repo nil path (js/JSON.stringify data nil 2) {:skip-compare? true}))))
 
@@ -354,30 +357,57 @@
 
 (def ^:export load_user_preferences
   (fn []
-    (p/let [repo ""
-            path (plugin-handler/get-ls-dotdir-root)
-            path (util/node-path.join path "preferences.json")
-            _    (fs/create-if-not-exists repo nil path)
-            json (fs/read-file nil path)
-            json (if (string/blank? json) "{}" json)]
-      (js/JSON.parse json))))
+    (let [repo ""
+          path (plugin-handler/get-ls-dotdir-root)
+          path (util/node-path.join path "preferences.json")]
+      (if (util/electron?)
+        (p/let [_ (fs/create-if-not-exists repo nil path)
+                json (fs/read-file nil path)
+                json (if (string/blank? json) "{}" json)]
+          (js/JSON.parse json))
+        (p/let [json (idb/get-item path)]
+          (or json #js {}))))))
 
 (def ^:export save_user_preferences
   (fn [^js data]
     (when data
-      (p/let [repo ""
-              path (plugin-handler/get-ls-dotdir-root)
-              path (util/node-path.join path "preferences.json")]
-        (fs/write-file! repo nil path (js/JSON.stringify data nil 2) {:skip-compare? true})))))
+      (let [repo ""
+            path (plugin-handler/get-ls-dotdir-root)
+            path (util/node-path.join path "preferences.json")]
+        (if (util/electron?)
+          (fs/write-file! repo nil path (js/JSON.stringify data nil 2) {:skip-compare? true})
+          (idb/set-item! path data))))))
 
 (def ^:export load_plugin_user_settings
   ;; results [path data]
-  (plugin-handler/make-fn-to-load-dotdir-json "settings" "{}"))
+  (plugin-handler/make-fn-to-load-dotdir-json "settings" #js {}))
 
 (def ^:export save_plugin_user_settings
   (fn [key ^js data]
     ((plugin-handler/make-fn-to-save-dotdir-json "settings")
-     key (js/JSON.stringify data nil 2))))
+     key data)))
+
+(defn ^:export load_installed_web_plugins
+ []
+ (let [getter (plugin-handler/make-fn-to-load-dotdir-json "installed-plugins-for-web" #js {})]
+        (some-> (getter :all) (p/then second))))
+
+(defn ^:export save_installed_web_plugin
+ ([^js plugin] (save_installed_web_plugin plugin false))
+ ([^js plugin remove?]
+  (when-let [id (some-> plugin (.-key) (name))]
+   (let [setter (plugin-handler/make-fn-to-save-dotdir-json "installed-plugins-for-web")
+         plugin (js/JSON.parse (js/JSON.stringify plugin))]
+    (p/let [^js plugins (or (load_installed_web_plugins) #js {})]
+           (if (true? remove?)
+            (when (aget plugins id)
+             (js-delete plugins id))
+            (gobj/set plugins id plugin))
+           (setter :all plugins))))))
+
+(defn ^:export unlink_installed_web_plugin
+ [key]
+ (save_installed_web_plugin #js {:key key} true))
 
 (def ^:export unlink_plugin_user_settings
   (plugin-handler/make-fn-to-unlink-dotdir-json "settings"))
@@ -1017,10 +1047,10 @@
 
 (def ^:export __install_plugin
   (fn [^js manifest]
-    (when-let [{:keys [repo id] :as mft} (bean/->clj manifest)]
+    (when-let [{:keys [repo id] :as manifest} (bean/->clj manifest)]
       (if-not (and repo id)
         (throw (js/Error. "[required] :repo :id"))
-        (plugin-common-handler/install-marketplace-plugin mft)))))
+        (plugin-common-handler/install-marketplace-plugin! manifest)))))
 
 ;; db
 (defn ^:export q

+ 7 - 0
yarn.lock

@@ -7499,6 +7499,13 @@ semver-greatest-satisfied-range@^1.1.0:
   resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.2.tgz#48d55db737c3287cd4835e17fa13feace1c41ef8"
   integrity sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==
 
[email protected]:
+  version "7.5.2"
+  resolved "https://registry.yarnpkg.com/semver/-/semver-7.5.2.tgz#5b851e66d1be07c1cdaf37dfc856f543325a2beb"
+  integrity sha512-SoftuTROv/cRjCze/scjGyiDtcUyxw1rgYQSZY7XTmtR5hX+dm76iDbTH8TkLPHCQmlbQVSSbNZCPM2hb0knnQ==
+  dependencies:
+    lru-cache "^6.0.0"
+
 semver@^6.0.0, semver@^6.2.0, semver@^6.3.1:
   version "6.3.1"
   resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"

Some files were not shown because too many files changed in this diff