-
Notifications
You must be signed in to change notification settings - Fork 0
Troubleshooting
Most generator problems fall into one of three groups:
- the command is running in the wrong Python environment or directory;
- generated files no longer match the marked source;
- the plugin builds locally but behaves differently inside PluginHost.
Start with the first two before debugging JNI or JSI.
From the plugin root, run:
sn-module-gen --version
sn-module-gen doctor --verbose
sn-module-gen check
sn-module-gen validate --all --verboseIf 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 --verboseReplace document with the feature's package name.
If Check reports drift, preview the complete canonical repair before writing:
sn-module-gen repair --dry-run --diffFor scripts and CI, add --json and use the command's exit code. Do not parse
the wording of human-readable output.
Check which Python environment contains the package:
python3 -m pip show sn-module-gen
python3 -m pip install --upgrade sn-module-genIf 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.
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.
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.
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 -versionIn Windows PowerShell:
java -version
$env:JAVA_HOME
.\android\gradlew.bat -versionInstalling 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.
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 --yesUse --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 \
--yesImporting 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 --verboseThen collect the feature name and nearby SupernoteModuleRuntime, ReactNativeJS,
and native-loading lines from adb logcat.
Validate the feature first:
sn-module-gen validate document --verboseValidation 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
Public C++, Kotlin, or Java visibility does not export anything by itself. The declaration needs deliberate Supernote intent:
// @SupernotePluginExport
double pageCount();@SupernotePluginExport
fun pageCount(): DoubleThen regenerate:
sn-module-gen update document --yesSupernotePluginInternal 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.
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
SupernoteExportObjectorSupernoteService; - 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
SupernoteConstructoron a free function; - putting a Supernote marker directly on a
.cdeclaration; - expecting Kotlin
suspendto implySupernotePluginAsync.
The generator reports these as source errors. It does not silently ignore or reinterpret recognized invalid intent.
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
numberwhereint64requiresbigint; - passing
ArrayBufferor another typed array where bytes requireUint8Array; - passing
nullat 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.
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 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.
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_EXHAUSTEDmeans the bounded work queue could not accept more work. -
CANCELLEDmeans cooperative cancellation won the operation. -
FEATURE_CLOSEDmeans the feature closed while the runtime was still healthy. -
IMPLEMENTATION_ERRORmeans user C++, Kotlin, or Java code failed unexpectedly. -
INTERNALmeans 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.
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.shOn Windows PowerShell:
.\android\gradlew.bat -p android :app:buildCustomApkDebug --no-daemon
powershell -ExecutionPolicy Bypass -File .\buildPlugin.ps1The 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
.snplgmodification time is new; - the root
PluginConfig.jsoncontains the intendedversionCodeandversionName; - 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.
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 logcatlines.
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.
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 -dThe 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 sendrebuilds and sends the JavaScript bundle; it does not rebuild or reinstall changed native code; -
npm run runlaunches or reopens the installed plugin; -
npm run diagnosticspreserves evidence without resetting the process; and -
npm run recoverresets PluginHost state when isolation is required.
Always collect diagnostics before recovery. A fresh-process pass cannot replace a same-process reload result.
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.
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.
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.
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.