Skip to content

Getting Started

s edited this page Aug 17, 2026 · 8 revisions

Getting Started

This page walks through adding one V2 module to an existing Supernote plugin, choosing an implementation environment, writing a function, generating the bindings, and calling it from JavaScript. The CLI calls each managed module a feature.

It assumes that the plugin already builds and runs. If it does not, fix the ordinary plugin build first so generator problems are not mixed together with plugin setup problems.

Before changing the plugin

Add creates a local package and connects it to the parent plugin. It can change:

local_modules/<package-name>/
package.json
package-lock.json or yarn.lock
android/settings.gradle or android/settings.gradle.kts
android/app/build.gradle or android/app/build.gradle.kts
android/.supernote-module/

There is no Add dry run. Before starting, commit your current work, save a patch, or otherwise make sure you can tell your existing changes apart from the generator's changes.

You do not need a perfectly clean repository. You just need a safe way to review and recover your own work.

Install the generator

Python 3.9 or newer is required.

On Linux or macOS:

python3 -m pip install --upgrade supernote-module-generator

On Windows, a common form is:

py -m pip install --upgrade supernote-module-generator

Confirm that the command is available:

supernote-module --version

For the current stable V2 startup behavior, that should report 2.0.3 or a later compatible release.

If the package installs but the command is not found, see Troubleshooting.

Run from the plugin root

Run the generator from the root of the existing plugin, not from android/, a source folder, or the generator repository.

The directory must contain at least:

PluginConfig.json
package.json
android/

The generator does not search parent directories for a plugin. A quick check on Linux, macOS, Git Bash, or WSL is:

pwd
ls PluginConfig.json package.json android

In PowerShell:

Get-Location
Get-Item PluginConfig.json, package.json, android

Run Doctor

Doctor checks the JavaScript and Android tools needed by the shared V2 runtime:

supernote-module doctor

This includes Java, Gradle, the Android SDK, Kotlin/KSP, CMake, the NDK, C23, C++23, and the local JSI build inputs.

Doctor checks the complete toolchain because one module can use the native environment, the JVM environment, or both. It does not use ADB and cannot prove that PluginHost on a target device will load and run the result.

Java 17 is the recommended Gradle JVM. V2 accepts Java 17 through 23; Java 17 and Java 21 were exercised in the platform builds. If several JDKs are installed, make sure java on PATH, JAVA_HOME, and any org.gradle.java.home setting all select the JDK you intend Gradle to use.

If Android Studio builds the plugin but Doctor cannot find the SDK, see Doctor cannot find Android tools.

Host platform notes

The generator and plugin build were checked on macOS ARM64, Linux Mint x86_64, and Windows 11 x86_64. These are host-side build results; they do not replace testing the finished plugin on its intended Supernote and PluginHost version.

Android C and C++ code is compiled by Android NDK Clang on every host. An ordinary Windows plugin build does not require MSVC or GCC. On Windows, keep the plugin in a reasonably short path such as C:\src\my-plugin. The generator keeps its own runtime paths short, but React Native and third-party CMake builds can still create long intermediate paths.

Open the guided CLI

For the normal interactive workflow, run:

supernote-module

The main menu contains:

Add feature
Update feature
Validate feature
Remove feature
Doctor
Help
Exit

Choose Add feature.

In a capable terminal, use the arrow keys and Enter. Press Esc to go back, or Ctrl+C to interrupt. In --plain mode, menus become numbered lists and accept :back or :cancel.

Choose an implementation environment

Add first asks which example source you want:

[ ] C/C++ (native)
[ ] Kotlin/Java (JVM)

You can select either one or both. These are two environments inside the same module, not two module types.

  • Choose C/C++ for native libraries, low-level processing, or C++ work.
  • Choose Kotlin/Java for Android APIs, JVM libraries, or Kotlin coroutines.
  • Choose both when the same module needs both kinds of work.

This is only scaffolding. A module created with one starter can gain the other environment later without conversion.

The rest of the guided flow asks for values such as:

Question Example What it controls
Package name document Folder under local_modules/, dependency name, and import string
Description Leave empty Optional package description
JavaScript name Document Generated module name inside the plugin runtime
Android namespace com.example.document Kotlin/Java namespace and generated Android paths
Package version 0.1.0 Version of this local module package
Install now Yes Runs npm or Yarn to refresh the local dependency

Add starts after the last valid answer. It does not show another confirmation screen.

Or run Add directly

The same choices are available without opening the main menu:

supernote-module add document --starter cpp --yes
supernote-module add document --starter kotlin --yes
supernote-module add document --starter cpp --starter kotlin --yes

--yes accepts documented defaults; it does not turn on every optional or destructive action. For the complete Add options, see Using the CLI.

Inspect what Add created

After Add finishes, inspect the plugin before writing more code:

git status --short
git diff

The generated module package normally looks like this:

local_modules/document/
├── .supernote-module.json
├── README.md
├── index.js
├── index.d.ts
├── package.json
└── android/src/main/
    ├── cpp/                       C and C++ implementation
    └── java/com/example/document/ Kotlin and Java implementation

Only the source roots you selected need to exist initially.

The plugin also gets one shared generated runtime below:

android/.supernote-module/v2-runtime/

That runtime belongs to the whole plugin. Adding three modules does not create three worker pools or three copies of the runtime.

Write one exported function

Normal source is ignored unless you deliberately mark a declaration for the generator.

Choose one of the following examples. You do not need to write both.

For the native environment, open android/src/main/cpp/feature.cpp and write:

#include <cstdint>

// @SupernotePluginExport
std::int32_t pageCount() {
  return 42;
}

For the JVM environment, open the generated FeatureApi.kt and write:

package com.example.document

import supernote.generated.annotations.SupernotePluginExport

@SupernotePluginExport
fun pageCount(): Int = 42

Do not mark both examples with the same public name in one module. V2 rejects cross-language duplicate API names instead of guessing which implementation you meant.

Anything without SupernotePluginExport or SupernotePluginInternal remains ordinary implementation code, even when it is public in its own language.

Generate and build

After changing marked source, run:

supernote-module update document --yes
supernote-module validate document --build --verbose

The Android build lets KSP inspect Kotlin/Java declarations, scans C++ markers, merges both environments into one module API, regenerates index.d.ts, and compiles the native/JVM routes.

validate without --build checks structure and integration only. That is useful for a quick check, but it is not enough after changing marked source.

Call it from the plugin

Inspect local_modules/document/index.d.ts for the exact generated API.

Import the generated package normally:

import document from 'document';

function onReadPageCount() {
  const pages: number = document.pageCount();
  // Use pages here.
}

The generated package can be imported while the JavaScript bundle is loading. It resolves the active native feature when your code uses the API. Keep actual native calls in the plugin's normal component, effect, or event flow rather than evaluating them as module-level constants before the plugin starts.

The implementation language does not change the JavaScript call. A C++ pageCount and a Kotlin pageCount produce the same public shape.

An explicitly async declaration returns a Promise instead:

const bytes: Uint8Array = await document.loadPage(3);

Read the language guide before adding async work or persistent objects:

Add the other environment later

Suppose you started in the native environment and later need Android Context. Add Kotlin or Java under the module's JVM source root, use the generated annotations, then update and build again. There is no “convert to JVM” command.

The reverse works as well: a JVM-started module can gain .c and .cpp files under its native source root.

For a module that deliberately connects native C++ to Kotlin or Java, see Using Both Environments.

Next steps

Clone this wiki locally