Anyone have a good example for a multiplatform lib...
# multiplatform
m
Anyone have a good example for a multiplatform library project that publishes to NPM? We have a use-case where we want to make a library in Kotlin and use it in a pure javascript repository. I tried this about a year ago and there were some rough edges, particularly around transitive library dependencies, that existed. I've read from bugfixes that it's better now, and I'd really like to see an example. Anyone have an idea?
b
Have you tried npm-publish plugin? Works like a charm with ir backend
1
You can even skip npm repo and just install your kotlin.js module into pure js module from generated tarball
There's a minimalistic example in the sandbox module there
m
Oh hey I remember you! You responded to a similar question with a similar answer around a year ago. Looks like your project has improved substantially. I'll definitely be taking a look again.
b
Oh, right 😀 From what I recall the issues were with having kotlin.js dependencies declared as bundledDependencies, which would only work with npm. With IR backend that's no longer an issue, since the compiler now embeds all transitive kotlin.js dependencies so the produced lib works everywhere as normal.
But give it a go yourself and let me know if you're still facing issues.
And here's your example https://github.com/jmfayard/kotlin-cli-starter. Good luck!
m
Yes, it was an issue with bundled deps originally. Glad to hear that's fixed! Embedding is unfortunately still not optimal, but as long as we only have one main kotlin library imported there shouldn't be any conflicts.
b
There won't be any conflicts either way. You can also have as many kotlin.js libs in it as you want. Remember that embedding only happens when you produce js binary and it goes through DCE, so all kotlin source code is treated as a massive monolithic codebase (your own code + kotlin libs). Although the final binary will be slightly bigger than it would be in plain js, since DCE is not perfect yet. Also note that dependencies you add via
implementation(npm("npm-module", "npm-version"))
are not embedded, but instead redeclared as regular js dependencies in final package.json
m
Oh yeah I get that. What I mean is that if we ever had two libraries in separate repository, library A may depend on version
1.2.3
of a library C and library B may need
1.5.0
of the same library C. If we depend on library A and B in our client application, then the internal dependencies might conflict between them. Or am I misunderstanding?
b
I'm assuming libraries A and B are kotlin.ls projects and client is plain js project consuming generated js binaries from A and B. In that case there won't be a conflict as npm/yarn will just have two different versions of common library available for different consumers. The only issue here is obviously increased cache size.
m
@Big Chungus I ran into an issue straight off the bat: https://github.com/mpetuska/npm-publish/issues/22
b
I think it's failing because you have both, nodejs and browser flavours defined. Can you try removing one of them?
👀 1
m
I kept the
nodejs
block and commented out the other block. Same issue.
Might be Gradle version. I'm going to try and upgrade to Gradle 7
b
I'm also trying to reproduce it on my end. Definitelly try with gradle 7+ and JDK 11+
m
Okay it is gradle version for sure
I fixed it
then ran into another problem which is a library compatibility issue I bet it'll be easy for you to fix.
We use the
nebula.release
plugin here to help version our projects. It is aware of git and assigns the versions internally. Link to Nebula Release Plugin. It makes the version object a
DelayedVersion
instead of a String, in case any changes need to be made to the version after the plugin is applied. You can solve this in a compatible way by using
version.toString()
in your code instead of
version
. Surprisingly, this is the first library I've ever had a problem with in this regard, so most other libs must be doing what I am suggesting with
.toString()
.
So, if you apply these two plugins:
Copy code
id("nebula.release") version "15.3.1"
    id("dev.petuska.npm.publish") version "2.0.3"
Then you'll see the failure I'm talking about.
I can also hackily handle this in my code with:
Copy code
plugins {
    id("nebula.release") version "15.3.1"
    id("dev.petuska.npm.publish") version "2.0.3"
}

version = version.toString()
Documented as,
Copy code
/**
 * Weird, but needed for dev.petuska.npm.publish, which does not .toString()
 * the version before using it. The nebula.release plugin uses a DelayedVersion
 * object, which we are converting to a string here.
 */
version = version.toString()
Okay yeah everything seems to be working now. I will test to make sure the produced package can be published, downloaded, and imported as expected and report back if there are issues. This is looking promising so far, though!
b
Thanks for the reports. I'm working on the new version right now! 😄
And that version thingy was really silly on my part. I kinda expected gradle to be more static with its types and forgot that it initially was built on groovy...
version as String?
->
version?.toString()
fixed it just as you said
m
Yeah I've had that bite me too, but turns out
version
is just
version: Object?
at its declaration. I had always assumed it had to be a string.
b
Here you go. You seem to no longer need it, but it should make your setup a tad cleaner anyways.
m
@Big Chungus I can confirm that version gets around the issue, thank you 🙂
🎉 1
@Big Chungus happy Friday! If you have a minute, I could use another spot of help -- hopefully the last for a while. I was able to publish the npm library, but when I went to use it, I realized that the JS file it contained had none of my class or package references inside. So I spent a couple hours putting together this example project to demonstrate what I'm seeing. https://github.com/mikeholler/kotlin-multiplatform-base64-example If you clone the above repository, then run
./gradlew pack
it builds the package in
build/publications/npm/js/
what I'd expect to see is one of the
.js
files containing the string
Base64Encoder
or
JsBase64Encoder
. Instead, I do not see anything that tells me that my code from
src/commonMain
and
src/jsMain
have been included in these JS files. Could you help me figure out what's going on here?
b
Ah, simple mistake, you're missing @JsExport annotations for stuff you want to be visible from JS (that annotation is available in common source-set)
Have a look at my
ts-consumer
and sandbox module examples in npm-publish repo
m
Oh that's encouraging... taking a look now
Ok, it appears that @JsExport does do the trick... kind of. It fails on expect/actual declarations, which is very important for me. I see that
@ExperimentalJsExport
exists, and it does not show compile errors, but it also doesn't put the code in the JS file. Do you have experience with this @Big Chungus?
b
Ah, I'm affraid annotations on expect are not automatically applied on actual, so make sure you annotate your js actuals
m
You can see the actuals here are annotated.
Hmm, wait a sec...
b
you definitelly need @JsExport (not experimental). Try annotating Base64Encoder with @JsExport as well
m
Okay so that's odd
b
Basically anything that's not annotated with @JsExport will be invisible to JS (that includes super-types of annotated members)
m
Yeah
I'm seeing that
Okay here's where we're at
@JsExport
works on the actual declarations, which is surprising because that's against what the docs say should happen. Hence the yellow warnings everywhere.
I cannot
@JsExport
the
Base64Encoder
class.
(compiler error)
But I can export everything else
b
You can drop @JsName annotations (@JsExport defaults to kotlin names) i think
m
Yeah I saw that
b
Also annotate Base64Encoder interface with JsName in common module
m
Just weird behavior, to cause a warning like that, and then to just work
b
However last time I played around with it (1.4.x) there were some issues with non-external jsExported interfaces.
m
Despite not being exportable, the interface does make its way into the
.js
file
b
I think compiler was generating mismatching .d.ts and .js files
m
Oh good call
The
.ds.ts
file is bad
So how would I fix that?
b
Try to only export external interfaces (it's fine to implement them on your internal kotlin objects
you can make Base64Encoder external on js via expect/actuals too
external kotlin interface fully vanishes on js, therefore nothing to break. Similar behaviour to TS interfaces...
m
But the
.ds.ts
mentions the interface in it, and you see red underlines. Isn't that a problem?
P.S. I have pushed new code to that sample repo capturing the current scenario
b
Base64Encoder interface is still not annotated with @JsExport (that's why it's not in .d.ts)
Again, you can use @JsExport on commonMain (as long as those are not expect's)
m
When I annotate
interface Base64Encoder
with
@JsExport
, I get a build error.
Copy code
e: /home/mjholler/Git/prototypes/public-multiplatform-base64/src/commonMain/kotlin/com/example/base64/Base64.kt: (7, 11): Declaration of such kind (interface) cant be exported to JS
I know it's a lot of information to process with these JsExports, but every tiny detail is important to get stuff working 😉
m
So you're saying I should do
@JsExport expect interface Base64Encoder
?
Trying to stitch it all together
b
More like
@JsExport actual external interface Base64Encode
m
Oh wow I did not know
external
was a keyword that could describe interfaces.
b
Two key points here: • external modifier • @JsExport annotation on actual declaration Both of the above should only be done in jsMain
Yeah, you can make anything external
But on js and native only. And to complicate things further, the keyword behaves differently on these two targets 😀
m
Oh fun.
So you can't make interfaces
expect
if it has functions with default declarations.
b
That's multiplatform for you. You need to be an expert in all platforms to use it 😀 That's how I learned js btw
m
Which, I guess, is fine and we can work around.
But I have no clue how to interpret this message now.
b
External interfaces cannot have bodies
😆 1
But you can create non-external Base64EncodeJs: Base64Encode that provides these default implementations
External kotlin interface behaves almost exactly the same as TS interface if you're familiar with those
m
Okay yeah, so we have a solution. Hurrah!
I did not think I'd have to use those kinds of mental gymnastics for this.
Thank you for your help.
b
Always glad to help
a
I might be missing something because the thread is quite long. I played a bit with publishing the Reaktive library to npm using the
npm-publish
plugin and failed. Basically the library has multiple Gradle modules. And my issue is: if module A depends on module B, and I publish locally both modules using IR mode, and I check the content of the published module A, then module A does not contain most of the public code. Also module A has some code from the module B embedded. I wrote down all my observation in the following issue: https://github.com/badoo/Reaktive/issues/488#issuecomment-873378640 Is it something known? Looks like an issue.
b
Give it another go, it's much better now. Also know that if you publish module A, it will not automatically reexport module B to js. Furthermore, some of the dead code is removed by DCE, so it's expected to not have everything in final bundle. Finally, make sure you use @JsExport properly.
I'll join that GH thread later today to explain some misconceptions you guys have
🙏 1