mirror of
https://github.com/logseq/logseq.git
synced 2026-04-30 17:06:23 +00:00
refactor: plugin libs (#12395)
This pull request refactors the plugin library infrastructure and adds new experimental features for hosted/sidebar renderers. The main changes include: Purpose: Refactor plugin communication library (Postmate) to support MessageChannel for improved performance, add support for hosted/sidebar renderers in plugins, add new debug APIs, and consolidate helper functions. Changes: Added MessageChannel support to Postmate for optimized plugin-host communication with backward compatibility Introduced hosted renderer and sidebar renderer APIs for plugins to register custom UI components Added new app APIs: get_current_route, export_debug_log_db, reset_debug_log_db Refactored helper functions from helpers.ts to common.ts and updated all import paths Extended block property APIs to include class properties with default values Added comprehensive documentation for experiments APIs and plugin development Added E2E test for plugin marketplace installation Version bump from 0.2.12 to 0.3.1
This commit is contained in:
460
libs/development-notes/experiments_api_guide.md
Normal file
460
libs/development-notes/experiments_api_guide.md
Normal file
@@ -0,0 +1,460 @@
|
||||
# Logseq Experiments API Guide
|
||||
|
||||
This guide covers the **experimental APIs** available in the Logseq Plugin SDK. These APIs provide advanced functionality for creating custom renderers, loading external scripts, and accessing internal utilities.
|
||||
|
||||
> **⚠️ WARNING**: These are experimental features that may change at any time. Plugins using these APIs may not be supported on the Marketplace temporarily.
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
|
||||
The Experiments API is accessed via `logseq.Experiments` and provides:
|
||||
|
||||
1. **React Integration** - Access to React and ReactDOM from the host
|
||||
2. **Custom Renderers** - Register custom code block, route, and daemon renderers
|
||||
3. **Component Access** - Access to internal Logseq components
|
||||
4. **Utilities** - ClojureScript interop utilities (toClj, toJs, etc.)
|
||||
5. **Script Loading** - Dynamic loading of external scripts
|
||||
6. **Extension Enhancers** - Enhance libraries like KaTeX and CodeMirror
|
||||
|
||||
---
|
||||
|
||||
## 1. React Integration
|
||||
|
||||
Access React and ReactDOM from the Logseq host environment.
|
||||
|
||||
### Properties
|
||||
|
||||
#### `logseq.Experiments.React`
|
||||
|
||||
Returns the React instance from the host scope.
|
||||
|
||||
```typescript
|
||||
const React = logseq.Experiments.React
|
||||
```
|
||||
|
||||
#### `logseq.Experiments.ReactDOM`
|
||||
|
||||
Returns the ReactDOM instance from the host scope.
|
||||
|
||||
```typescript
|
||||
const ReactDOM = logseq.Experiments.ReactDOM
|
||||
```
|
||||
|
||||
### Example Usage
|
||||
|
||||
```typescript
|
||||
const React = logseq.Experiments.React
|
||||
const ReactDOM = logseq.Experiments.ReactDOM
|
||||
|
||||
// Use React to create components
|
||||
const MyComponent = React.createElement('div', null, 'Hello from plugin!')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Components
|
||||
|
||||
Access internal Logseq components for advanced UI integration.
|
||||
|
||||
### `logseq.Experiments.Components.Editor`
|
||||
|
||||
A page editor component that can render Logseq page content.
|
||||
|
||||
**Type**: `(props: { page: string } & any) => any`
|
||||
|
||||
**Parameters**:
|
||||
- `page` (string): The page name to render
|
||||
|
||||
```typescript
|
||||
const Editor = logseq.Experiments.Components.Editor
|
||||
|
||||
// Render a page editor
|
||||
const editor = Editor({ page: 'My Page Name' })
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Utilities
|
||||
|
||||
ClojureScript interop utilities for data conversion between JavaScript and ClojureScript.
|
||||
|
||||
### `logseq.Experiments.Utils`
|
||||
|
||||
Provides conversion utilities:
|
||||
|
||||
#### `toClj(input: any)`
|
||||
|
||||
Convert JavaScript data to ClojureScript data structures.
|
||||
|
||||
```typescript
|
||||
const cljData = logseq.Experiments.Utils.toClj({ key: 'value' })
|
||||
```
|
||||
|
||||
#### `jsxToClj(input: any)`
|
||||
|
||||
Convert JSX/JavaScript objects to ClojureScript, preserving JSX structures.
|
||||
|
||||
```typescript
|
||||
const cljData = logseq.Experiments.Utils.jsxToClj(<div>Content</div>)
|
||||
```
|
||||
|
||||
#### `toJs(input: any)`
|
||||
|
||||
Convert ClojureScript data structures to JavaScript.
|
||||
|
||||
```typescript
|
||||
const jsData = logseq.Experiments.Utils.toJs(cljData)
|
||||
```
|
||||
|
||||
#### `toKeyword(input: any)`
|
||||
|
||||
Convert a string to a ClojureScript keyword.
|
||||
|
||||
```typescript
|
||||
const keyword = logseq.Experiments.Utils.toKeyword('my-key')
|
||||
```
|
||||
|
||||
#### `toSymbol(input: any)`
|
||||
|
||||
Convert a string to a ClojureScript symbol.
|
||||
|
||||
```typescript
|
||||
const symbol = logseq.Experiments.Utils.toSymbol('my-symbol')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Script Loading
|
||||
|
||||
### `logseq.Experiments.loadScripts(...scripts: string[])`
|
||||
|
||||
Dynamically load external scripts into the Logseq environment.
|
||||
|
||||
**Parameters**:
|
||||
- `scripts` (string[]): Array of script URLs or relative paths
|
||||
|
||||
**Returns**: `Promise<void>`
|
||||
|
||||
**Behavior**:
|
||||
- Relative paths are resolved using the plugin's resource path
|
||||
- HTTP/HTTPS URLs are loaded directly
|
||||
- Scripts are loaded in order
|
||||
|
||||
```typescript
|
||||
// Load external library
|
||||
await logseq.Experiments.loadScripts(
|
||||
'https://cdn.jsdelivr.net/npm/chart.js@3.9.1/dist/chart.min.js'
|
||||
)
|
||||
|
||||
// Load local script from plugin resources
|
||||
await logseq.Experiments.loadScripts('./my-script.js')
|
||||
|
||||
// Load multiple scripts
|
||||
await logseq.Experiments.loadScripts(
|
||||
'https://cdn.example.com/lib1.js',
|
||||
'https://cdn.example.com/lib2.js',
|
||||
'./local-script.js'
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Custom Renderers
|
||||
|
||||
### 5.1 Fenced Code Renderer
|
||||
|
||||
Register a custom renderer for code blocks with specific language tags.
|
||||
|
||||
#### `logseq.Experiments.registerFencedCodeRenderer(lang: string, opts: object)`
|
||||
|
||||
**Parameters**:
|
||||
- `lang` (string): The language identifier for the code block (e.g., 'mermaid', 'chart')
|
||||
- `opts` (object):
|
||||
- `render` (function, required): Render function that receives props
|
||||
- `edit` (boolean, optional): Whether the block is editable
|
||||
- `before` (function, optional): Async function to run before rendering
|
||||
- `subs` (string[], optional): Subscriptions to state changes
|
||||
|
||||
**Render Props**:
|
||||
- `content` (string): The content of the code block
|
||||
|
||||
```typescript
|
||||
// Register a custom code block renderer
|
||||
logseq.Experiments.registerFencedCodeRenderer('my-chart', {
|
||||
edit: false,
|
||||
before: async () => {
|
||||
// Load dependencies before rendering
|
||||
await logseq.Experiments.loadScripts(
|
||||
'https://cdn.jsdelivr.net/npm/chart.js'
|
||||
)
|
||||
},
|
||||
render: (props) => {
|
||||
const React = logseq.Experiments.React
|
||||
|
||||
return React.createElement('div', {
|
||||
ref: (el) => {
|
||||
if (el) {
|
||||
// Parse content and render chart
|
||||
const config = JSON.parse(props.content)
|
||||
new Chart(el, config)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
**Usage in Logseq**:
|
||||
````markdown
|
||||
```my-chart
|
||||
{
|
||||
"type": "bar",
|
||||
"data": {
|
||||
"labels": ["A", "B", "C"],
|
||||
"datasets": [{"data": [10, 20, 30]}]
|
||||
}
|
||||
}
|
||||
```
|
||||
````
|
||||
|
||||
### 5.2 Daemon Renderer
|
||||
|
||||
Register a renderer that runs continuously in the background (daemon).
|
||||
|
||||
#### `logseq.Experiments.registerDaemonRenderer(key: string, opts: object)`
|
||||
|
||||
**Parameters**:
|
||||
- `key` (string): Unique identifier for the daemon renderer
|
||||
- `opts` (object):
|
||||
- `render` (function, required): Render function
|
||||
- `sub` (string[], optional): Subscriptions to state changes
|
||||
|
||||
```typescript
|
||||
// Register a daemon renderer for persistent UI
|
||||
logseq.Experiments.registerDaemonRenderer('my-status-bar', {
|
||||
sub: ['ui/theme', 'ui/sidebar-open'],
|
||||
render: (props) => {
|
||||
const React = logseq.Experiments.React
|
||||
|
||||
return React.createElement('div', {
|
||||
style: {
|
||||
position: 'fixed',
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
padding: '10px',
|
||||
background: '#333',
|
||||
color: '#fff'
|
||||
}
|
||||
}, 'Status: Active')
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### 5.3 Route Renderer
|
||||
|
||||
Register a custom renderer for specific routes in Logseq.
|
||||
|
||||
#### `logseq.Experiments.registerRouteRenderer(key: string, opts: object)`
|
||||
|
||||
**Parameters**:
|
||||
- `key` (string): Unique identifier for the route renderer
|
||||
- `opts` (object):
|
||||
- `path` (string, required): Route path (e.g., '/my-plugin-page')
|
||||
- `render` (function, required): Render function
|
||||
- `name` (string, optional): Display name for the route
|
||||
- `subs` (string[], optional): Subscriptions to state changes
|
||||
|
||||
```typescript
|
||||
// Register a custom route
|
||||
logseq.Experiments.registerRouteRenderer('my-custom-page', {
|
||||
path: '/my-plugin-dashboard',
|
||||
name: 'Dashboard',
|
||||
subs: ['ui/theme'],
|
||||
render: (props) => {
|
||||
const React = logseq.Experiments.React
|
||||
|
||||
return React.createElement('div', {
|
||||
className: 'my-plugin-dashboard'
|
||||
}, [
|
||||
React.createElement('h1', null, 'Plugin Dashboard'),
|
||||
React.createElement('p', null, 'Custom content here')
|
||||
])
|
||||
}
|
||||
})
|
||||
|
||||
// Navigate to the route
|
||||
logseq.App.pushState('page', { name: 'my-plugin-dashboard' })
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Extension Enhancers
|
||||
|
||||
Enhance external libraries that Logseq uses (like KaTeX for math rendering).
|
||||
|
||||
### `logseq.Experiments.registerExtensionsEnhancer(type: string, enhancer: function)`
|
||||
|
||||
**Parameters**:
|
||||
- `type` ('katex' | 'codemirror'): The extension type to enhance
|
||||
- `enhancer` (function): Async function that receives the library instance and can modify it
|
||||
|
||||
**Returns**: `Promise<void>`
|
||||
|
||||
```typescript
|
||||
// Enhance KaTeX with custom macros
|
||||
logseq.Experiments.registerExtensionsEnhancer('katex', async (katex) => {
|
||||
// Add custom KaTeX macros
|
||||
katex.macros = {
|
||||
...katex.macros,
|
||||
'\\RR': '\\mathbb{R}',
|
||||
'\\NN': '\\mathbb{N}',
|
||||
'\\ZZ': '\\mathbb{Z}'
|
||||
}
|
||||
|
||||
console.log('KaTeX enhanced with custom macros')
|
||||
})
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Plugin Local Access
|
||||
|
||||
### `logseq.Experiments.pluginLocal`
|
||||
|
||||
Access the internal plugin instance (PluginLocal) for advanced operations.
|
||||
|
||||
**Type**: `PluginLocal`
|
||||
|
||||
```typescript
|
||||
const pluginLocal = logseq.Experiments.pluginLocal
|
||||
|
||||
// Access plugin-specific internal state
|
||||
console.log('Plugin ID:', pluginLocal.id)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Advanced: Invoke Experimental Methods
|
||||
|
||||
### `logseq.Experiments.invokeExperMethod(type: string, ...args: any[])`
|
||||
|
||||
Directly invoke experimental methods from the host scope.
|
||||
|
||||
**Parameters**:
|
||||
- `type` (string): Method name (converted to snake_case)
|
||||
- `...args`: Arguments to pass to the method
|
||||
|
||||
**Returns**: `any`
|
||||
|
||||
```typescript
|
||||
// Invoke a custom experimental method
|
||||
const result = logseq.Experiments.invokeExperMethod(
|
||||
'someExperimentalFeature',
|
||||
arg1,
|
||||
arg2
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Complete Example: Custom Chart Renderer
|
||||
|
||||
Here's a complete example combining multiple APIs:
|
||||
|
||||
```typescript
|
||||
import '@logseq/libs'
|
||||
|
||||
async function main() {
|
||||
console.log('Chart Plugin Loaded')
|
||||
|
||||
// Register fenced code renderer for charts
|
||||
logseq.Experiments.registerFencedCodeRenderer('chart', {
|
||||
edit: false,
|
||||
before: async () => {
|
||||
// Load Chart.js before rendering
|
||||
await logseq.Experiments.loadScripts(
|
||||
'https://cdn.jsdelivr.net/npm/chart.js@3.9.1/dist/chart.min.js'
|
||||
)
|
||||
},
|
||||
render: (props) => {
|
||||
const React = logseq.Experiments.React
|
||||
|
||||
return React.createElement('div', null, [
|
||||
React.createElement('canvas', {
|
||||
ref: (canvas) => {
|
||||
if (canvas && window.Chart) {
|
||||
try {
|
||||
const config = JSON.parse(props.content)
|
||||
new window.Chart(canvas, config)
|
||||
} catch (e) {
|
||||
console.error('Chart rendering error:', e)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
logseq.ready(main).catch(console.error)
|
||||
```
|
||||
|
||||
**Usage**:
|
||||
````markdown
|
||||
```chart
|
||||
{
|
||||
"type": "line",
|
||||
"data": {
|
||||
"labels": ["Jan", "Feb", "Mar", "Apr"],
|
||||
"datasets": [{
|
||||
"label": "Sales",
|
||||
"data": [10, 20, 15, 30],
|
||||
"borderColor": "rgb(75, 192, 192)"
|
||||
}]
|
||||
}
|
||||
}
|
||||
```
|
||||
````
|
||||
|
||||
---
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Check Host Scope**: Always ensure the host scope is accessible before using experimental APIs
|
||||
2. **Error Handling**: Wrap experimental API calls in try-catch blocks
|
||||
3. **Dependencies**: Load external scripts in `before` hooks to ensure they're ready
|
||||
4. **Memory Management**: Clean up event listeners and subscriptions in daemon renderers
|
||||
5. **Compatibility**: Test thoroughly as these APIs may change between Logseq versions
|
||||
6. **Documentation**: Document which experimental APIs your plugin uses
|
||||
7. **Marketplace**: Be aware that plugins using these APIs may not be accepted on the Marketplace
|
||||
|
||||
---
|
||||
|
||||
## Limitations
|
||||
|
||||
- **Experimental Status**: These APIs are not stable and may change without notice
|
||||
- **Marketplace Support**: Plugins using experimental APIs may not be approved for the Marketplace
|
||||
- **Security**: Be cautious when loading external scripts or accessing host scope
|
||||
- **Performance**: Custom renderers can impact performance if not optimized
|
||||
- **Compatibility**: Limited backwards compatibility guarantees
|
||||
|
||||
---
|
||||
|
||||
## See Also
|
||||
|
||||
- [Starter Guide](./starter_guide.md) - Getting started with plugin development
|
||||
- [DB Properties Guide](./db_properties_guide.md) - Working with database properties
|
||||
- [DB Query Guide](./db_query_guide.md) - Querying the Logseq database
|
||||
|
||||
---
|
||||
|
||||
## Support
|
||||
|
||||
For questions and issues:
|
||||
- [Logseq Discord](https://discord.gg/logseq) - #plugin-dev channel
|
||||
- [GitHub Discussions](https://github.com/logseq/logseq/discussions)
|
||||
- [Plugin API Documentation](https://plugins-doc.logseq.com/)
|
||||
|
||||
Remember: These are experimental features. Use at your own risk and always test thoroughly!
|
||||
125
libs/development-notes/starter_guide.md
Normal file
125
libs/development-notes/starter_guide.md
Normal file
@@ -0,0 +1,125 @@
|
||||
# Logseq Plugin Starter Guide
|
||||
|
||||
# For Developers
|
||||
|
||||
In this short guide, it will walk you through the steps needed to set up your development environment for writing and
|
||||
running a hello world simple inside of Logseq Desktop Client. You know Logseq Plugin based on Web Technologies composed
|
||||
of JS & HTML & CSS, but neither are mandatory, you can develop plugins use any language which can be translated to
|
||||
javascript (Logseq made by Clojurescript!). We will use Typescript to demonstrate this sample. Because there
|
||||
is [a type definition file](https://www.npmjs.com/package/@logseq/libs) to make development experience even better.
|
||||
|
||||
## Install Node.js and NPM
|
||||
|
||||
You can download Node.js here, which will include NPM: https://nodejs.org/en/download/.
|
||||
|
||||
## Install TypeScript
|
||||
|
||||
To install TypeScript, run `npm install -g typescript` in a terminal.
|
||||
|
||||
## Get Logseq desktop app
|
||||
|
||||
At this time, plugin development and testing needs to be done using the Logseq desktop app. This is because Logseq needs
|
||||
to read your code saved as a local file. The Logseq desktop app can be downloaded
|
||||
here: https://github.com/logseq/logseq/releases.
|
||||
|
||||
If you already have the desktop app, please make sure to update to the latest version, as several features have been
|
||||
added specifically in order to provide a better plugin development experience.
|
||||
|
||||
## Go to Settings > Turn on Developer Mode
|
||||
|
||||
This will bring up the "Plugins" entry in three dots more menu list on top right of head bar. Go to Plugins page, and
|
||||
you will get a button with `Load unpacked plugin` label.
|
||||
|
||||
## Create plugin package from scaffold
|
||||
|
||||
We will use [logseq-hello-world](https://github.com/logseq/logseq-plugin-samples/tree/master/logseq-hello-world) as
|
||||
scaffold. Download this package files using Git or download zip straight to your own plugin package directory.
|
||||
|
||||
## Explain `package.json`
|
||||
|
||||
Generally Logseq plugin package is described by `package.json` file located root. Let's checkout these primary keys.
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "logseq-plugin-hello-world",
|
||||
"version": "0.0.1",
|
||||
"main": "dist/index.html",
|
||||
"logseq": {
|
||||
"id": "yet-another-hello-world-plugin",
|
||||
"title": "Say Hi to Logseq",
|
||||
"icon": "logseq.png"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- __name__ [required] a name of plugin.
|
||||
- it starts with `logseq-plugin` is better for Googling.
|
||||
- __version__ [required] semantic version
|
||||
- related link: https://semver.org/
|
||||
- __main__ [optional] entry of plugin, optional for theme plugin only.
|
||||
- `.html` file entry indicate that the plugin provide main iframe ui.
|
||||
- `.js` file entry indicate that the plugin without main iframe ui.
|
||||
- __logseq__ [required] specs of plugin & indicate it's a Logseq package.
|
||||
- `id` [optional] the identity key of plugin. if you don't provide it that will be auto generated on load
|
||||
development package.
|
||||
- `title` [optional] the title display of plugin list card. if not provided, value of `pkg#name` would be used.
|
||||
- `icon` [optional] plugin logo for your own brand :D.
|
||||
- `minSDKVersion` - [optional] the minimum sdk version what your plugin required. eg: `0.1.0`.
|
||||
- `supportsDB` - [optional] the boolean value indicate whether your plugin support the database feature.
|
||||
- `supportsDBOnly` - [optional] the boolean value indicate whether your plugin only support the database feature.
|
||||
|
||||
Apart from these primary keys, you might find other useful fields, e.g. `author`, `description`, `repository`.
|
||||
Although these are not necessary, but completed fields are kind for your plugin users.
|
||||
|
||||
## Develop your code
|
||||
|
||||
This scaffold using [Parcel](https://v2.parceljs.org/) as `ts` bundler, just it works out of box. Of Course, you could
|
||||
choose another one what you prefer. Then install dependencies `npm install` and watch your code in development mode
|
||||
`npm run dev`. It will build the development version outputs to the dist directory. Go to plugins page and pick your
|
||||
plugin root directory by `Load unpacked plugin` button. You will get a message from this plugin. Let's explain the entry
|
||||
file of `index.ts`.
|
||||
|
||||
### 1. import user plugin SDK [required]
|
||||
|
||||
```js
|
||||
import '@logseq/libs'
|
||||
```
|
||||
|
||||
It provides TS types and global sdk namespace `logseq`.
|
||||
|
||||
### 2. bootstrap your main function [required]
|
||||
|
||||
```js
|
||||
logseq.ready(main).catch(console.error)
|
||||
```
|
||||
|
||||
Limited by the plugin system mechanism, user main function should wait for some preparations to make sure the SDK api
|
||||
works correctly!
|
||||
|
||||
### 3. show your code [required]
|
||||
|
||||
```js
|
||||
function main () {
|
||||
logseq.UI.showMsg('❤️ Message from Hello World Plugin :)')
|
||||
}
|
||||
```
|
||||
|
||||
Change your code and reload the plugin to make it works.
|
||||
|
||||
> If you develop main ui in iframe, [HMR](https://webpack.js.org/concepts/hot-module-replacement/) feature provided by
|
||||
> some ui frameworks is convenient that can avoid reloading plugin again and again.
|
||||
|
||||
🎉 Actually, you're done!
|
||||
|
||||
## Next steps
|
||||
|
||||
- Awesome samples
|
||||
- https://github.com/logseq/logseq-plugin-samples
|
||||
- https://github.com/logseq/awesome-logseq
|
||||
- Community plugins
|
||||
- https://github.com/search?q=logseq-plugin
|
||||
|
||||
# For users
|
||||
|
||||
Currently, Logseq plugin can only run in developer mode. Under this mode, plugin has ability to access notes data. So if
|
||||
you're working with sensitive data, you'd better confirm the plugin is from trusted resource before installing it.
|
||||
Reference in New Issue
Block a user