How to convert Javascript exported class to Kotlin/JS?
Asked Answered
N

1

6

I am new to JS and to Kotlin/JS. I have the following minimal working Javascript code for a Plugin for Obsidian from an example. It works as expected:

var obsidian = require('obsidian');
class SomePlugin extends obsidian.Plugin {
    onload() {
        new obsidian.Notice('This is a notice!');
    }
}
module.exports = Plugin;

I was hoping to extend this plugin using Kotlin as I know the language, but I have some problems converting this to Kotlin/JS. My approach so far:

The runnable project can be found here on Github. Run gradle build to generate the build folder. It will fail in the browser step, but that step is not necessary. After the build the generated js file can be found in build\js\packages\main\kotlin\main.js.

main.kt

@JsExport
class SomePlugin: Plugin() {
    override fun onload() {
        Notice("This is a notice!")
    }
}
@JsModule("obsidian")
@JsNonModule // required by the umd moduletype
external open class Component {
    open fun onload()
}
@JsModule("obsidian")
@JsNonModule
external open class Plugin : Component {
}
@JsModule("obsidian")
@JsNonModule
external open class Notice(message: String, timeout: Number = definedExternally) {
    open fun hide()
}

Edit: Thanks to the comment of @S.Janssen I switched the module type to umd

build.gradle.kts

plugins {
    kotlin("js") version "1.5.20"
}
group = "de.example"
version = "1.0-SNAPSHOT"
repositories {
    mavenCentral()
}
dependencies {
    implementation(npm("obsidian", "0.12.5", false))
}
kotlin {
    js(IR) {
        binaries.executable()
        browser {
            webpackTask {
                output.libraryTarget = "umd"
            }
        }
    }
}

tasks.withType<KotlinJsCompile>().configureEach {
    kotlinOptions.moduleKind = "umd"
}

I don't actually need a result that can be run in the browser, but without the browser definition, it would not even generate a js file. With the browser part, an exception is thrown saying Can't resolve 'obsidian' in 'path\kotlin'. But at least a .js file is created under build/js/packages/test/kotlin/test.js. However the code is completely different from my expected code and also is not accepted by obsidian as a valid plugin code. I also tried some other gradle options. like "umd", "amd", "plain", legacy compiler instead of IR, nodejs instead of browser. But nothing creates a runnable js file. The error messages differ. With the legacy compiler it requires the kotlin.js file, that it cannot find even if I put it right next to it in the folder or copy the content into the script.

How do I get code functionally similar to the Javascript code posted above? I understand that it will have overhead, but the code currently generated does not even define or export my class by my understanding.

The error message that I get from obisidan debugger:

Plugin failure: obsidian-sample-plugin TypeError: Object prototype may only be an Object or null: undefined

The code generated:

    (function (root, factory) {
  if (typeof define === 'function' && define.amd)
    define(['exports', 'obsidian', 'obsidian', 'obsidian'], factory);
  else if (typeof exports === 'object')
    factory(module.exports, require('obsidian'), require('obsidian'), require('obsidian'));
  else {
    if (typeof Component === 'undefined') {
      throw new Error("Error loading module 'main'. Its dependency 'obsidian' was not found. Please, check whether 'obsidian' is loaded prior to 'main'.");
    }if (typeof Plugin === 'undefined') {
      throw new Error("Error loading module 'main'. Its dependency 'obsidian' was not found. Please, check whether 'obsidian' is loaded prior to 'main'.");
    }if (typeof Notice === 'undefined') {
      throw new Error("Error loading module 'main'. Its dependency 'obsidian' was not found. Please, check whether 'obsidian' is loaded prior to 'main'.");
    }root.main = factory(typeof main === 'undefined' ? {} : main, Component, Plugin, Notice);
  }
}(this, function (_, Component, Plugin, Notice) {
  'use strict';
  SomePlugin.prototype = Object.create(Plugin.prototype);
  SomePlugin.prototype.constructor = SomePlugin;
  function Unit() {
    Unit_instance = this;
  }
  Unit.$metadata$ = {
    simpleName: 'Unit',
    kind: 'object',
    interfaces: []
  };
  var Unit_instance;
  function Unit_getInstance() {
    if (Unit_instance == null)
      new Unit();
    return Unit_instance;
  }
  function SomePlugin() {
    Plugin.call(this);
  }
  SomePlugin.prototype.onload_sv8swh_k$ = function () {
    new Notice('This is a notice!');
    Unit_getInstance();
  };
  SomePlugin.prototype.onload = function () {
    return this.onload_sv8swh_k$();
  };
  SomePlugin.$metadata$ = {
    simpleName: 'SomePlugin',
    kind: 'class',
    interfaces: []
  };
  _.SomePlugin = SomePlugin;
  return _;
}));
Nordine answered 7/7, 2021 at 21:11 Comment(2)
The ja snipper does export in _.SomePlugin = SomePlugin... Thats the umd/amd/plain stuff. After the generated code executes you can import SomePlugin.I do not know kotlin/Js or obsidian, there fore i cannot really answer with certainty right now from mobile, but try to change these JsModule("obsidian") to JsModule("obsidian.plugin") etc. Since the generated js seems to not access the classes from the library but rather access the library as if it were all these classes...Ambulatory
@S.Janssen Hi, thanks for taking your time to try help me. So good to know that this is everything but commonjs, so I switched to umd. I will update my question with the changed code. Using "obsidian.plugin" however yields the message that "Cannot find module 'obsidian.plugin'", which doesn't happen with just "obisidan" so I didn't change that.Nordine
N
4

You can find a working example of what you're going for here. I'll go through some of the changes that needed to be made to your code one-by-one in this reply.

Being unable to resolve obsidian

Can't resolve 'obsidian' in 'path\kotlin' occurs because the obsidian-api package is not a standalone library. Instead, it only consist of a obsidian.d.ts file, which is a TypeScript declaration file. Similar to a header file in other languages, this header file does not provide any implementations, but only the signatures and types for the library – meaning Kotlin/JS' webpack (or any JavaScript tooling, for that matter) won't be able to resolve the actual implementations. This is expected, and can be addressed by declaring the module as external. To do so in Kotlin/JS, create a directory called webpack.config.d, and add a file 01.externals.js with the following content:

config.externals = {
    obsidian: 'obsidian',
};

(You can actually find an equivalent snippet in the offical sample-plugin configuration, as well, since this isn't a Kotlin/JS specific problem)

Grouping multiple @JsModule declarations

Because you're importing multiple declarations from the same package, instead of annotating multiple signatures with @JsModule / @JsNonModule, you'll have to create a separate file, and annotate it with @file:@JsModule("...") / @file:JsNonModule:

@file:JsModule("obsidian")
@file:JsNonModule

open external class Component {
    open fun onload()
    open fun onunload()
}

open external class Plugin(
    app: Any,
    manifest: Any
) : Component

open external class Notice(message: String, timeout: Number = definedExternally) {
    open fun hide()
}

Kotlin's ES5 vs Obsidian's ES6

Additionally, some of your problems stem from the fact that Obsidian's examples implicitly make the assumption that you are targeting ES6 (while Kotlin's current target is ES5). Specifically, this makes a difference in regards to how your plugin exports its members, as well as how classes are instantiated.

Inheritance

In regards to inheritance (since YourPlugin inherits from Plugin), ES6 classes automatically initialize the parent class with all arguments. This is something that is not supported in ES5's prototype inheritance. This is why in the snippet above, we need to explicitly pass the Plugin class constructor the app and manifest parameters, and pass them through in the implementation of your specific plugin:

class SomePlugin(
    app: Any,
    manifest: Any
) : Plugin(
    app,
    manifest
)

Exports / Module System

In regards to exporting your plugin, Obsidian expects either module.exports or exports.default to be your Plugin class directly. To achieve this exact export behavior, a few conditions need to be met, which is unfortunately a bit cumbersome: - The library target needs to be CommonJS: output.libraryTarget = "commonjs" (not CommonJS2) - To prevent creating a level of indirection, as is usually the case, the exported library need to be set to null: output.library = null - To export your Plugin under as default, its class declaration needs to be marked as @JsName("default").

Newsome answered 12/7, 2021 at 13:7 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.