Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,36 @@ jobs:
name: butterfly-nextcloud
path: dist/nextcloud

joplin:
name: Joplin
runs-on: ubuntu-latest
timeout-minutes: 15

steps:
- name: Check out repository
uses: actions/checkout@v7

- name: Set up pnpm
uses: pnpm/action-setup@v6

- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version: '24.11'
cache: pnpm

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Build Joplin plugin
run: pnpm check:joplin

- name: Upload Joplin build
uses: actions/upload-artifact@v6
with:
name: butterfly-joplin
path: integrations/joplin/publish

php:
name: PHP
runs-on: ubuntu-latest
Expand Down
135 changes: 135 additions & 0 deletions .github/workflows/release-joplin.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
name: Release Joplin integration

on:
push:
tags:
- 'joplin/v*'
workflow_dispatch:
inputs:
version:
description: Version to package without the leading v
required: true
type: string
publish:
description: Publish the versioned and stable GitHub releases
required: true
default: false
type: boolean

permissions:
contents: write

concurrency:
group: release-joplin
cancel-in-progress: false

jobs:
release:
name: Build and release Joplin
runs-on: ubuntu-latest
timeout-minutes: 20

steps:
- name: Check out repository
uses: actions/checkout@v7

- name: Resolve release version
id: release
env:
DISPATCH_VERSION: ${{ inputs.version }}
run: |
if [[ "${GITHUB_REF_TYPE}" == "tag" ]]; then
version="${GITHUB_REF_NAME#joplin/v}"
else
version="${DISPATCH_VERSION#v}"
fi
if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then
echo "Invalid version: $version" >&2
exit 1
fi
if [[ "$version" == *-* ]]; then
stable=false
prerelease=true
else
stable=true
prerelease=false
fi
echo "version=$version" >> "$GITHUB_OUTPUT"
echo "stable=$stable" >> "$GITHUB_OUTPUT"
echo "prerelease=$prerelease" >> "$GITHUB_OUTPUT"

- name: Verify integration versions
env:
RELEASE_VERSION: ${{ steps.release.outputs.version }}
run: node scripts/integration-version.mjs joplin "$RELEASE_VERSION"

- name: Set up pnpm
uses: pnpm/action-setup@v6

- name: Set up Node.js
uses: actions/setup-node@v6
with:
node-version: '24.11'
cache: pnpm

- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: TypeScript typecheck
run: pnpm --filter joplin-plugin-butterfly exec tsc --noEmit

- name: Build shared package and Joplin plugin
run: |
pnpm --filter @linwood/butterfly-integration-shared check
pnpm check:joplin

- name: Create release archives
run: pnpm package:joplin

- name: Create release notes from changelog
env:
RELEASE_VERSION: ${{ steps.release.outputs.version }}
run: node scripts/changelog-entry.mjs joplin "$RELEASE_VERSION" dist/joplin/release-notes.md

- name: Upload workflow artifact
uses: actions/upload-artifact@v6
with:
name: butterfly-joplin-v${{ steps.release.outputs.version }}
path: dist/joplin

- name: Publish GitHub release
if: github.ref_type == 'tag' || inputs.publish
uses: softprops/action-gh-release@v3
with:
tag_name: joplin/v${{ steps.release.outputs.version }}
name: Joplin v${{ steps.release.outputs.version }}
body_path: dist/joplin/release-notes.md
prerelease: ${{ steps.release.outputs.prerelease }}
make_latest: false
files: |
dist/joplin/dev.linwood.butterfly-joplin.jpl
dist/joplin/dev.linwood.butterfly-joplin.json
dist/joplin/checksums.txt

- name: Move stable tag to this release
if: ${{ (github.ref_type == 'tag' || inputs.publish) && steps.release.outputs.stable == 'true' }}
env:
STABLE_TAG: joplin/stable
run: |
git tag --force "$STABLE_TAG" "$GITHUB_SHA"
git push origin "refs/tags/$STABLE_TAG" --force

- name: Update stable GitHub release
if: ${{ (github.ref_type == 'tag' || inputs.publish) && steps.release.outputs.stable == 'true' }}
uses: softprops/action-gh-release@v3
with:
tag_name: joplin/stable
name: Joplin Stable
body_path: dist/joplin/release-notes.md
prerelease: true
make_latest: false
overwrite_files: true
files: |
dist/joplin/dev.linwood.butterfly-joplin.jpl
dist/joplin/dev.linwood.butterfly-joplin.json
dist/joplin/checksums.txt
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,9 @@ window and origin.

### 📝 Joplin

Planned. The directory is reserved, but the integration is not implemented or
built yet.
Open, create, edit, and save Butterfly documents from within a Joplin panel.

[**Learn more**](integrations/joplin/README.md)

### 💎 Obsidian

Expand All @@ -57,7 +58,7 @@ built yet.
```text
integrations/
nextcloud/ # implemented and built in CI
joplin/ # reserved; not built
joplin/ # implemented and built in CI
obsidian/ # reserved; not built
packages/
shared/ # framework-neutral TypeScript helpers
Expand All @@ -78,7 +79,7 @@ pnpm check
```

See the [Nextcloud integration guide](integrations/nextcloud/README.md) for PHP,
Composer, and Docker setup. Joplin and Obsidian are intentionally excluded from
Composer, and Docker setup. Obsidian is intentionally excluded from
the workspace until their implementations exist.

## Releases
Expand Down
3 changes: 3 additions & 0 deletions integrations/joplin/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
dist/
node_modules/
publish/
8 changes: 8 additions & 0 deletions integrations/joplin/.npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
*.md
!README.md
/*.jpl
/api
/src
/dist
tsconfig.json
webpack.config.js
18 changes: 18 additions & 0 deletions integrations/joplin/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [0.1.0] - 2026-09-26

### Added

- Open and edit `.bfly` documents in a joplin panel.
- Load and save documents through the Butterfly embed protocol.
- Attach canvas exports as `.svg` to your notes.
- Set the origin (URL) for the butterfly embed as a joplin setting.
- Set the language for the butterfly embed as a joplin setting.
78 changes: 78 additions & 0 deletions integrations/joplin/GENERATOR_DOC.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Plugin development

This documentation describes how to create a plugin, and how to work with the plugin builder framework and API.

## Installation

First, install [Yeoman](http://yeoman.io) and generator-joplin using [npm](https://www.npmjs.com/) (we assume you have pre-installed [node.js](https://nodejs.org/)).

```bash
npm install -g yo@4.3.1
npm install -g generator-joplin
```

Then generate your new project:

```bash
yo --node-package-manager npm joplin
```

## Structure

The main two files you will want to look at are:

- `/src/index.ts`, which contains the entry point for the plugin source code.
- `/src/manifest.json`, which is the plugin manifest. It contains information such as the plugin a name, version, etc.

The file `/plugin.config.json` could also be useful if you intend to use [external scripts](#external-script-files), such as content scripts or webview scripts.

## Building the plugin

The plugin is built using Webpack, which creates the compiled code in `/dist`. A JPL archive will also be created at the root, which can use to distribute the plugin.

To build the plugin, simply run `npm run dist`.

The project is setup to use TypeScript, although you can change the configuration to use plain JavaScript.

## Updating the manifest version number

You can run `npm run updateVersion` to bump the patch part of the version number, so for example 1.0.3 will become 1.0.4. This script will update both the package.json and manifest.json version numbers so as to keep them in sync.

## Publishing the plugin

To publish the plugin, add it to npmjs.com by running `npm publish`. Later on, a script will pick up your plugin and add it automatically to the Joplin plugin repository as long as the package satisfies these conditions:

- In `package.json`, the name starts with "joplin-plugin-". For example, "joplin-plugin-toc".
- In `package.json`, the keywords include "joplin-plugin".
- In the `publish/` directory, there should be a .jpl and .json file (which are built by `npm run dist`)

In general all this is done automatically by the plugin generator, which will set the name and keywords of package.json, and will put the right files in the "publish" directory. But if something doesn't work and your plugin doesn't appear in the repository, double-check the above conditions.

## Updating the plugin framework

To update the plugin framework, run `npm run update`.

In general this command tries to do the right thing - in particular it's going to merge the changes in package.json and .gitignore instead of overwriting. It will also leave "/src" as well as README.md untouched.

The file that may cause problem is "webpack.config.js" because it's going to be overwritten. For that reason, if you want to change it, consider creating a separate JavaScript file and include it in webpack.config.js. That way, when you update, you only have to restore the line that include your file.

## External script files

By default, the compiler (webpack) is going to compile `src/index.ts` only (as well as any file it imports), and any other file will simply be copied to the plugin package. In some cases this is sufficient, however if you have [content scripts](https://joplinapp.org/api/references/plugin_api/classes/joplincontentscripts.html) or [webview scripts](https://joplinapp.org/api/references/plugin_api/classes/joplinviewspanels.html#addscript) you might want to compile them too, in particular in these two cases:

- The script is a TypeScript file - in which case it has to be compiled to JavaScript.

- The script requires modules you've added to package.json. In that case, the script, whether JS or TS, must be compiled so that the dependencies are bundled with the JPL file.

To get such an external script file to compile, you need to add it to the `extraScripts` array in `plugin.config.json`. The path you add should be relative to /src. For example, if you have a file in "/src/webviews/index.ts", the path should be set to "webviews/index.ts". Once compiled, the file will always be named with a .js extension. So you will get "webviews/index.js" in the plugin package, and that's the path you should use to reference the file.

## More information

- [Joplin Plugin API](https://joplinapp.org/api/references/plugin_api/classes/joplin.html)
- [Joplin Data API](https://joplinapp.org/help/api/references/rest_api)
- [Joplin Plugin Manifest](https://joplinapp.org/api/references/plugin_manifest/)
- Ask for help on the [forum](https://discourse.joplinapp.org/) or our [Discord channel](https://discord.gg/VSj7AFHvpq)

## License

MIT © Laurent Cozic
53 changes: 49 additions & 4 deletions integrations/joplin/README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,52 @@
# Butterfly for Joplin

This directory reserves the Joplin integration in the monorepo. It is not
implemented, installed, built, or released yet.
This Joplin plugin opens and edits `.bfly` files with Butterfly's supported
embed API. Documents are loaded into the editor only while they are open.
The plugin is based on the official [Joplin plugin template](https://github.com/laurent22/joplin/commits/dev/packages/generator-joplin/generators/app/templates) at commit
`e58fcf9402973d8b9f5ae7b340b9b426cf07988e`.

When development starts, add its package here and opt it into the root pnpm
workspace and CI explicitly.
## Requirements

- Joplin 3.6 or newer

## Build

```bash
corepack enable
pnpm install --frozen-lockfile
cd integrations/joplin
pnpm run dist
```

This creates the plugin in `publish/dev.linwood.butterfly-joplin.jpl`

## Checks

```bash
pnpm --filter @linwood/butterfly-integration-shared check
pnpm check:joplin
```

See [the repository contribution guide](../../CONTRIBUTING.md) for the complete
development workflow.

## How documents are handled

The plugin adds an action for clicking Markdown image-links in the form of
!\[bfly@<documentId>](previewId). Doing so will open a joplin panel.
It reads the file from the joplin resource directory, sends the bytes to the
Butterfly iframe with `postMessage`, and writes bytes back when Butterfly emits `save`
by storing them in the temporary directory of the os under `joplin-butterfly-plugin/`
and then uploading them to the joplin resource directory.

By default, the editor iframe uses `https://preview.butterfly.linwood.dev/embed`.
The user may configure another Butterfly origin under **Tools → Options → Butterfly**.

Exiting does not save the document but closes the joplin panel containing the iframe.
Only messages from that exact origin and iframe window are accepted.

## Contributing and security

Contributions are welcome. Read [CONTRIBUTING.md](../../CONTRIBUTING.md) before
opening a pull request. Please report vulnerabilities according to
[SECURITY.md](../../SECURITY.md), rather than through a public issue.
19 changes: 19 additions & 0 deletions integrations/joplin/api/Global.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import Plugin from '../Plugin';
import Joplin from './Joplin';
import BasePlatformImplementation from '../BasePlatformImplementation';
import type {Store} from 'redux';
/**
* @ignore
*/
/**
* @ignore
*/
export default class Global {
private joplin_;

constructor(implementation: BasePlatformImplementation, plugin: Plugin, store: Store<any>);

get joplin(): Joplin;

get process(): NodeJS.Process;
}
Loading