|
| 1 | +# Use JavaScript modules from Kotlin |
| 2 | + |
| 3 | +* **Type**: Kotlin JS design proposal |
| 4 | +* **Author**: Zalim Bashorov |
| 5 | +* **Contributors**: Andrey Breslav, Alexey Andreev |
| 6 | +* **Status**: Submitted |
| 7 | +* **Prototype**: |
| 8 | + |
| 9 | +## Goal |
| 10 | + |
| 11 | +Provide the way to add information about related JS module when write native declarations in Kotlin |
| 12 | +and use this information to generate dependencies. |
| 13 | + |
| 14 | +## Intro |
| 15 | + |
| 16 | +In general case JS module can be: |
| 17 | + - object with a bunch of declarations (in terms of kotlin it can be package or object) |
| 18 | + - class |
| 19 | + - function |
| 20 | + - variable |
| 21 | + |
| 22 | +Additionally we should keep in mind that module import string should not be valid identifier. |
| 23 | + |
| 24 | + |
| 25 | +## Proposed solution |
| 26 | + |
| 27 | +Propose to add: |
| 28 | + |
| 29 | +```kotlin |
| 30 | +@Repeatable |
| 31 | +@Retention(AnnotationRetention.BINARY) |
| 32 | +@Target( |
| 33 | + AnnotationTarget.CLASS, |
| 34 | + AnnotationTarget.PROPERTY, |
| 35 | + AnnotationTarget.FUNCTION, |
| 36 | + AnnotationTarget.FILE) |
| 37 | +annotation class JsModule( |
| 38 | + val import: String, |
| 39 | + vararg val kind: JsModuleKind = arrayOf(JsModuleKind.AMD, JsModuleKind.COMMON_JS, JsModuleKind.UMD) |
| 40 | +) |
| 41 | + |
| 42 | +enum class JsModuleKind { |
| 43 | + SIMPLE, |
| 44 | + AMD, |
| 45 | + COMMON_JS, // CJS? |
| 46 | + UMD |
| 47 | +} |
| 48 | +``` |
| 49 | + |
| 50 | +It should be **repeatable** to allow to specialize settings for concrete JsModuleKind. |
| 51 | + |
| 52 | +It should have **binary retention** to be available from binary data. |
| 53 | + |
| 54 | +Annotation **allowed on classes, properties and functions** according to what can be exported from JS modules. |
| 55 | + |
| 56 | +In JS world objects often used as namespace/package so will be nice to allow to map such objects to Kotlin packages. |
| 57 | + |
| 58 | +Why just not using objects in all cases? |
| 59 | +1. Some IDE features works better with package level declarations (e.g. auto import) |
| 60 | +2. Accessing to package must not have side effects, so in some cases we can generate better code (alternative solution is add marker annotation) |
| 61 | + |
| 62 | +So to achieve that files allowed as target of the annotation. |
| 63 | +But it's not enough, additionally we should provide the way to specify custom qualifier for native declarations in the file. |
| 64 | + |
| 65 | +It can be achieved by adding yet another parameter to the annotation or add a new annotation, like: |
| 66 | + |
| 67 | +```kotlin |
| 68 | +@Retention(AnnotationRetention.BINARY) |
| 69 | +@Target(AnnotationTarget.FILE) |
| 70 | +annotation class JsPackage(val path: String) |
| 71 | +``` |
| 72 | +[TODO] think up a better name. |
| 73 | + |
| 74 | +The new annotation can be reused in case when we want to have package with long path in Kotlin, |
| 75 | +but don't want to have long path in generated JS, e.g. for public API. |
| 76 | +Of course, this problem can be fixed by adding another file "facade" with short qualifier. |
| 77 | + |
| 78 | + |
| 79 | +Parameters: |
| 80 | +- `import` -- string which will be used to import related module. |
| 81 | +- `kind` -- shows for which kind of modules this role should be applied. |
| 82 | + |
| 83 | + |
| 84 | +## Use cases (based on TypeScript declarations) |
| 85 | + |
| 86 | +**Simple module declaration** |
| 87 | + |
| 88 | +Code in TypeScript: |
| 89 | +```typescript |
| 90 | +declare module "MyExternalModule" { |
| 91 | + export function foo(); |
| 92 | + export var bar; |
| 93 | + export namespace baz { |
| 94 | + function boo(); |
| 95 | + } |
| 96 | +} |
| 97 | +``` |
| 98 | + |
| 99 | +In Koltin it can be written like: |
| 100 | +```kotlin |
| 101 | +// file1.kt |
| 102 | +@file:JsModule("MyExternalModule") |
| 103 | +package MyExternalModule |
| 104 | + |
| 105 | +@native fun foo() {} |
| 106 | +@native var bar: Any = noImpl; |
| 107 | +``` |
| 108 | +```kotlin |
| 109 | +// file2.kt |
| 110 | +@file:JsModule("MyExternalModule") |
| 111 | +@file:JsPackage("baz") |
| 112 | +package MyExternalModule.baz |
| 113 | + |
| 114 | +@native fun boo() {} |
| 115 | +``` |
| 116 | + |
| 117 | +**Export by assignment** |
| 118 | + |
| 119 | +In TypeScript: |
| 120 | +```typescript |
| 121 | +declare module "MyExternalModule" { |
| 122 | + export = function foo(); |
| 123 | +} |
| 124 | +``` |
| 125 | + |
| 126 | +In Kotlin: |
| 127 | +```kotlin |
| 128 | +package MyExternalModule |
| 129 | + |
| 130 | +@JsModule("MyExternalModule") |
| 131 | +@native fun foo() {} |
| 132 | +``` |
| 133 | + |
| 134 | +**Export by assignment form toplevel** |
| 135 | + |
| 136 | +In TypeScript: |
| 137 | +```typescript |
| 138 | +declare var prop: MyClass; |
| 139 | +export = prop; |
| 140 | +``` |
| 141 | + |
| 142 | +In Kotlin: |
| 143 | +```kotlin |
| 144 | +package SomeModule |
| 145 | + |
| 146 | +@JsModule("SomeModule") |
| 147 | +@native var prop: MyClass = noImpl |
| 148 | +``` |
| 149 | + |
| 150 | +**Export by assignment the declaration decelerated outside of module** |
| 151 | + |
| 152 | +In TypeScript: |
| 153 | +```typescript |
| 154 | +declare var prop: MyClass; |
| 155 | + |
| 156 | +declare module "MyExternalModule" { |
| 157 | + export = prop; |
| 158 | +} |
| 159 | +``` |
| 160 | + |
| 161 | +In Kotlin: |
| 162 | +```kotlin |
| 163 | +package SomeModule |
| 164 | + |
| 165 | +@JsModule("MyExternalModule") |
| 166 | +@JsModule("MyExternalModule", kind = JsModuleKind.SIMPLE) |
| 167 | +@native var prop: MyClass = noImpl |
| 168 | +``` |
| 169 | +Second role means that when translate with SIMPLE module kind for this module |
| 170 | +compiler should generate import through variable `MyExternalModule` |
| 171 | + |
| 172 | + |
| 173 | +Another way: |
| 174 | +```kotlin |
| 175 | +package SomeModule |
| 176 | + |
| 177 | +@JsModule("MyExternalModule") |
| 178 | +@JsModule("this", kind = JsModuleKind.SIMPLE) |
| 179 | +@native var prop: MyClass = noImpl |
| 180 | +``` |
| 181 | +And now when translate with SIMPLE module kind for this module |
| 182 | +compiler should generate import through variable `this` (usually it's Global Object) |
| 183 | + |
| 184 | + |
| 185 | +## Implementation details |
| 186 | + |
| 187 | +### Backend |
| 188 | + |
| 189 | +Let's consider the following files: |
| 190 | + |
| 191 | +**declarations1.kt** |
| 192 | +```kotlin |
| 193 | +@file:JsModule("first-module") |
| 194 | + |
| 195 | +var a: Int = noImpl |
| 196 | +``` |
| 197 | + |
| 198 | +**declarations2.kt** |
| 199 | +```kotlin |
| 200 | +@JsModule("second-module") |
| 201 | +var b: Int = noImpl |
| 202 | +``` |
| 203 | + |
| 204 | +**declarations3.kt** |
| 205 | +```kotlin |
| 206 | +@file: JsModule("thirdModule") |
| 207 | + |
| 208 | +var c: Int = noImpl |
| 209 | +``` |
| 210 | + |
| 211 | +**declarations4.kt** |
| 212 | +```kotlin |
| 213 | +@JsModule("fourthModule") |
| 214 | +var d: Int = noImpl |
| 215 | +``` |
| 216 | + |
| 217 | +**usage.kt** |
| 218 | +```kotlin |
| 219 | +fun test() { |
| 220 | + println(a) |
| 221 | + println(b) |
| 222 | + println(c) |
| 223 | + println(d) |
| 224 | +} |
| 225 | +``` |
| 226 | + |
| 227 | +Use import value as is to declare dependencies when translate them with **any module kind except SIMPLE**. |
| 228 | +<br/>E.g. for CommonJS generate following: |
| 229 | +```javascript |
| 230 | +var first_module = require("first-module"); |
| 231 | +var b = require("second-module"); |
| 232 | +var thirdModule = require("thirdModule"); |
| 233 | +var d = require("fourthModule"); |
| 234 | +//... |
| 235 | +``` |
| 236 | + |
| 237 | +When **module kind is SIMPLE** |
| 238 | +- If import value is valid JS identifier or `this` then use it as is as name of identifier; |
| 239 | +- Otherwise, try to get from `this` using import value as is (as string). |
| 240 | + |
| 241 | +```javascript |
| 242 | +(function(first_module, b, thirdModule, d) { |
| 243 | +// ... |
| 244 | +}(this["first-module"], this["second-module"], thirdModule, fourthModule)); |
| 245 | +``` |
| 246 | + |
| 247 | +### Frontend |
| 248 | +- Report error when try to use native declaration which has `JsModule` annotations, but no one specify rule for current module kind. |
| 249 | +- Prohibit to have many annotations which explicitly provide the same module kind. |
| 250 | +- Prohibit to apply `JsModule` annotation to non-native declarations, except files.<br/> |
| 251 | +It can be relaxed later e.g. to reuse this annotation to allow translate a file to separate JS module, it can be useful to interop with some frameworks (see [KT-12093](https://youtrack.jetbrains.com/issue/KT-12093)) |
| 252 | + |
| 253 | +### IDE |
| 254 | +- Add inspection for case when some declarations with the same fq-name have different JsModule annotations? |
| 255 | + Consider next cases: |
| 256 | + - function overloads |
| 257 | + - package and functions |
| 258 | + |
| 259 | +## Open questions |
| 260 | +1. Can we introduce default value for `import` parameter of `JsModule` and use the name of declaration as import string when argument not provided?<br/> |
| 261 | +If so, how it should work when the annotation used on file? |
| 262 | + |
| 263 | +2. What should be default value of `kind` parameter of `JsModule`? |
| 264 | + 1. all kinds |
| 265 | + 2. all kinds except SIMPLE |
| 266 | + |
| 267 | + In TypeScript (external) modules can be used only when compiler ran with module kind (in our terms it's all except SIMPLE). |
| 268 | + So, should second be default to generate simpler code from TS declarations? |
| 269 | + |
| 270 | +3. Actually right now we needs to know only is `kind === SIMPLE` or not, so should we simplify API? |
| 271 | + |
| 272 | +4. Unfortunately we can't use constants for `kind` parameter to make API better. Can we fix it somehow? |
| 273 | + |
| 274 | +5. Will be nice to have the way to say that all declarations in this file is native. But how it fit with idea to replace `@native` with `external`? |
0 commit comments