Files
logseq/libs/development-notes/experiments_api_guide.md
Charlie a95483655b 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
2026-04-14 14:29:22 +08:00

461 lines
11 KiB
Markdown

# 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!