Skip to content

Troubleshooting

s edited this page Aug 30, 2026 · 15 revisions

Troubleshooting

Most generator problems fall into one of three groups:

  1. the command is running in the wrong Python environment or directory;
  2. generated files no longer match the marked source;
  3. the plugin builds locally but behaves differently inside PluginHost.

Start with the first two before debugging JNI or JSI.

Try this first

From the plugin root, run:

sn-module-gen --version
sn-module-gen doctor --verbose
sn-module-gen check
sn-module-gen validate --all --verbose

If you recently changed a marker, type, constructor, or source file, regenerate the feature too:

sn-module-gen update document --yes
sn-module-gen validate document --build --verbose

Replace document with the feature's package name.

If Check reports drift, preview the complete canonical repair before writing:

sn-module-gen repair --dry-run --diff

For scripts and CI, add --json and use the command's exit code. Do not parse the wording of human-readable output.

sn-module-gen is not found

Check which Python environment contains the package:

python3 -m pip show sn-module-gen
python3 -m pip install --upgrade sn-module-gen

If pip finds the distribution but the command is still missing, its scripts directory is not on PATH. Activating the virtual environment that installed it is usually the simplest fix.

The generator says this is not a plugin root

Run the command from the directory that contains all three of these:

PluginConfig.json
package.json
android/

The generator intentionally does not search parent directories. This keeps an add, update, or remove command from changing the wrong plugin.

Doctor cannot find Android tools

If Android Studio builds but Doctor cannot find the SDK, export the SDK path in the same terminal:

# Linux
export ANDROID_HOME="$HOME/Android/Sdk"

# macOS
export ANDROID_HOME="$HOME/Library/Android/sdk"

Doctor also checks Java, Gradle, Kotlin/KSP, C23/C++23, the NDK, CMake, Node, the package manager, and JSI inputs. Read the first failed check instead of changing several tool versions at once.

After moving a project from macOS to Linux, rebuild local dependencies and generated build output. Gradle, CMake, NDK, Android, and JavaScript caches often contain absolute paths from the old machine. Do not copy android/local.properties unless its SDK path is valid on the new machine.

Doctor selects an ADB executable and runs adb version as an advisory host-tool probe. It does not connect to a device or certify PluginHost or tablet behavior.

Gradle is using the wrong Java version

Java 17 is the recommended Gradle JVM. The generator accepts Java 17 through 23. Java 11 is too old, and Java 24 or newer is outside the supported range.

Check the Java selected by your shell and by Gradle. On Linux or macOS:

java -version
echo "$JAVA_HOME"
./android/gradlew -version

In Windows PowerShell:

java -version
$env:JAVA_HOME
.\android\gradlew.bat -version

Installing Java 17 does not change a stale JAVA_HOME or org.gradle.java.home setting. Run sn-module-gen doctor --verbose from the plugin root to see which effective Gradle JVM fails the generator's check.

add asks for missing choices

In non-interactive mode, the feature package is required. Without --yes, the command also needs every choice that would affect generated output.

This is a complete minimal command:

sn-module-gen add document --starter cpp --yes

Use --starter kotlin for a Kotlin example, or repeat --starter to create both. Starter choices only scaffold files; they do not turn the feature into a permanent C++ or JVM backend.

If both package-lock.json and yarn.lock exist, clean up the plugin's package manager state or choose explicitly:

sn-module-gen add document \
  --starter kotlin \
  --package-manager npm \
  --yes

A feature call says is not installed in the Supernote generated runtime

Importing a generated package at the top of a JavaScript or TypeScript file is supported. The package resolves the active feature when code reads or calls its API.

This error now means one of two things:

  • code accessed a generated function or class as a module-level value before PluginHost installed the native runtime; or
  • native integration failed and the runtime was never installed.

Keep the import at the top of the file, but make the actual call from the plugin's normal component, effect, or event flow. Do not replace a normal import with an arbitrary delayed require().

If an ordinary component or event call still fails, run:

sn-module-gen validate document --build --verbose

Then collect the feature name and nearby SupernoteModuleRuntime, ReactNativeJS, and native-loading lines from adb logcat.

JavaScript cannot find the feature package

Validate the feature first:

sn-module-gen validate document --verbose

Validation checks that the feature is linked from the plugin's package.json and installed in node_modules. If installation was skipped or dependencies were removed, run the plugin's normal package-manager install and validate again.

Import the generated npm package name, not the source directory or Android namespace. You can find that name in:

local_modules/document/package.json

A function is missing from index.d.ts

Public C++, Kotlin, or Java visibility does not export anything by itself. The declaration needs deliberate Supernote intent:

// @SupernotePluginExport
double pageCount();
@SupernotePluginExport
fun pageCount(): Double

Then regenerate:

sn-module-gen update document --yes

SupernotePluginInternal is deliberately absent from TypeScript. It creates a generated cross-language route, not a JavaScript API.

If two declarations in different languages use the same exported name, the generator reports a collision instead of choosing one.

A marker or annotation is rejected

The generated marker names are:

SupernotePluginExport
SupernotePluginInternal
SupernotePluginAsync
SupernotePluginObject
SupernotePluginValue
SupernoteConstructor

C++ uses exact own-line comments, for example:

// @SupernotePluginExport
// @SupernotePluginAsync
std::vector<std::byte> loadPage(std::int32_t page);

Kotlin and Java import generated annotations from supernote.generated.annotations.

Common errors include:

  • using an old or made-up name such as SupernoteExportObject or SupernoteService;
  • adding arguments or aliases to a C++ marker;
  • combining export and internal intent on the same declaration;
  • using async without export or internal intent;
  • putting SupernoteConstructor on a free function;
  • putting a Supernote marker directly on a .c declaration;
  • expecting Kotlin suspend to imply SupernotePluginAsync.

The generator reports these as source errors. It does not silently ignore or reinterpret recognized invalid intent.

A parameter or result type is rejected

The generated boundary supports this closed family of types:

void
bool
int32
int64
float32
float64
string
bytes
string enum
declared value object
native reference object
typed array
nullable value

Use richer types inside ordinary implementation code, then convert them at the marked boundary.

Frequent JavaScript-side mistakes are:

  • passing number where int64 requires bigint;
  • passing ArrayBuffer or another typed array where bytes require Uint8Array;
  • passing null at a position that was not declared nullable;
  • passing a fractional or out-of-range number to an integer parameter.

Wrong argument count or JavaScript type throws TypeError. Invalid numeric shape or range throws RangeError.

A class does not expose the methods you expected

Marking a class exposes the object type. It does not expose every public method:

// @SupernotePluginObject
class Document {
public:
  // @SupernoteConstructor
  Document(std::string path);

  // @SupernotePluginExport
  std::int32_t pageCount() const;

  void clearCache();  // ordinary C++; ignored
};

Only a constructor explicitly marked with SupernoteConstructor becomes the normal create path. Mark at most one eligible public constructor. A class without one is returned-only, and constructors cannot be async.

Object parameters/results, returned-only objects, live marked fields, and exported factory-shaped functions/methods are supported. Object transport is same-family in the current generator; inheritance and arbitrary JavaScript objects remain unsupported. Use the generated .is, .accepts, and .checkArguments APIs to inspect or route uncertain values safely.

The runtime keeps an accepted async method's receiver alive, but it does not lock the object's state. If calls can overlap, make the implementation thread-safe or serialize access yourself.

A synchronous call freezes the plugin UI

A synchronous export runs on the JavaScript thread. It is fine for quick lookups or small calculations, but a file read, database call, long loop, or blocking JVM call can freeze the UI.

If the work can block, make the API deliberately async:

// @SupernotePluginExport
// @SupernotePluginAsync
std::vector<std::byte> loadPage(std::int32_t page);

A normal blocking implementation then runs on the shared bounded worker executor. A supported Kotlin suspend implementation uses its coroutine adapter.

An async Promise rejects

Once arguments pass validation and an async operation is accepted, failures use one SupernoteError shape. Check its stable code:

RESOURCE_EXHAUSTED
CANCELLED
FEATURE_CLOSED
IMPLEMENTATION_ERROR
INTERNAL
  • RESOURCE_EXHAUSTED means the bounded work queue could not accept more work.
  • CANCELLED means cooperative cancellation won the operation.
  • FEATURE_CLOSED means the feature closed while the runtime was still healthy.
  • IMPLEMENTATION_ERROR means user C++, Kotlin, or Java code failed unexpectedly.
  • INTERNAL means generated/runtime machinery failed or found a broken invariant.

There is no public AbortSignal or custom cancellable operation object yet. Internal teardown cancellation is cooperative. A native or JVM call that cannot stop may finish physically, but its late result is discarded.

During runtime shutdown, a completion may be dropped instead of rejecting. The JavaScript realm that owned the Promise is already disappearing, so touching JSI would be unsafe.

See Error Handling for complete try/catch examples, recommended responses for each stable code, and internal C++ supernote::Result<T> handling.

Gradle succeeds but the packaged plugin is stale

Do not use a packaging script's exit code as the only build check. Run the strict Android target first:

./android/gradlew -p android :app:buildCustomApkDebug --no-daemon
bash buildPlugin.sh

On Windows PowerShell:

.\android\gradlew.bat -p android :app:buildCustomApkDebug --no-daemon
powershell -ExecutionPolicy Bypass -File .\buildPlugin.ps1

The Android native build uses Android NDK Clang. It does not require MSVC or GCC. If CMake reports path-length or intermediate-file failures on Windows, move the plugin to a shorter path such as C:\src\my-plugin and rebuild.

Then check that:

  • the .snplg modification time is new;
  • the root PluginConfig.json contains the intended versionCode and versionName;
  • the package contains the expected JavaScript bundle; and
  • its nested app.npk, ABI, and shared libraries are current.

An old package can survive if a script logs a Gradle or copy failure and continues. A packaging script's successful exit is not enough by itself.

Before installing changed code, increase the root plugin version. The feature-level --package-version is separate and does not tell PluginHost that the complete .snplg is a newer install.

The installed plugin still shows old code

Use this order when installing an update:

change marked source
    -> run sn-module-gen update
    -> run the strict Gradle build
    -> increase the root PluginConfig version
    -> package and inspect the new .snplg
    -> install it

If the correctly rebuilt and versioned package still appears stale, restart PluginHost without clearing its data as a diagnostic step. Do not make clearing all PluginHost data the normal update procedure.

The current generator uses generation-specific native bindings and a repeatable classloader-local installation path instead of depending only on the first JNI_OnLoad. PluginHost installation and package caching can still affect which artifact is active. When reporting an update problem, capture:

  • device model and firmware;
  • generator and root plugin versions;
  • PluginHost process ID before and after the test;
  • whether the first load worked;
  • whether same-process reload worked;
  • the complete related adb logcat lines.

Do not use copied native libraries as a reload workaround; they can fail to resolve PluginHost's JSI library namespace. Also do not report a fresh-process success as proof that reload works.

Device behavior is only confirmed after testing on the intended Supernote and PluginHost version.

Threads grow after repeated bundle replacement

The current generator starts its generated worker and cleanup services only when work needs them. Invalidating the last session for one native runtime generation stops and joins those services. Repeated JavaScript bundle replacement should therefore not add another complete worker pool for every generation.

PluginHost and React Native may still create or retire their own threads, and PluginHost retains changed native generations up to the documented process limit. Judge a trend from checkpoints rather than one sample. For a repeatable test, record the same PluginHost PID and collect:

adb shell pidof com.ratta.supernote.pluginhost
adb shell cat /proc/<pid>/status
adb shell ps -T -p <pid>
adb logcat -d

The generated runtime logs stopped process services for runtime generation after final-session invalidation. A reload campaign should have one such marker for each retired generation and no repeated groups of generated executor/cleanup threads left behind.

Use the plugin template scripts according to what they actually do:

  • npm run send rebuilds and sends the JavaScript bundle; it does not rebuild or reinstall changed native code;
  • npm run run launches or reopens the installed plugin;
  • npm run diagnostics preserves evidence without resetting the process; and
  • npm run recover resets PluginHost state when isolation is required.

Always collect diagnostics before recovery. A fresh-process pass cannot replace a same-process reload result.

Closing the plugin did not cancel its work

PluginManager.closePluginView() can hide the plugin UI without destroying its JavaScript runtime or Supernote feature session. On the tested PluginHost, closing the view returned to Notes but kept the PluginHost process and mounted JavaScript state alive. Accepted native work continued, queued Promise callbacks ran, and reopening showed the existing plugin state.

Do not treat closing the view as a guaranteed:

  • React component unmount;
  • feature or runtime teardown;
  • cancellation request; or
  • immediate native-resource cleanup.

Accepted asynchronous work may continue after the UI disappears. If actual feature teardown occurs while the runtime remains healthy, the generated runtime applies its feature-close Promise and cancellation rules. If the runtime itself is invalidated, late work cannot touch JSI. Merely hiding the view does not necessarily trigger either path.

The current generator does not expose AbortSignal or another caller-controlled cancellation API, so do not invent a hidden cancellation argument around this behavior.

An update replaced a generated edit

That is expected. Edit files the feature owns, such as C/C++ and Kotlin/Java implementation source. Treat generated adapters, manifests, JSI/JNI bindings, runtime files, Gradle fragments, and TypeScript declarations as outputs.

If generated output is wrong, change the marked source or generator. Do not maintain a private patch inside a generated file.

See Managing Modules for the ownership rules.

A transaction was interrupted

Do not delete .supernote-module-transaction.json or its staging directory. Run the next generator command or Doctor and follow the printed recovery step.

Exit code 3 means the command made partial progress and recovery is required. Preserve the plugin and journal before attempting anything manually.

When reporting a problem

Include the command, complete output, generator version, host operating system, and the smallest marked declaration that reproduces it.

Also say which level actually passed:

Result What it proves
Generator tests Source/model behavior on that computer
Gradle, KSP, CMake, and NDK build Discovery, generation, and compilation
TypeScript check Generated public type surface
Package inspection The expected files reached the package
Target-device first load Initial PluginHost integration
Same-process reload and lifecycle tests Runtime replacement and late-work behavior

A local build does not prove device loading, and a device first load does not prove same-process reload.

Clone this wiki locally