Skip to content

Commit 1c78663

Browse files
committed
Add proposal "Use JavaScript modules from Kotlin"
1 parent e37c89e commit 1c78663

File tree

2 files changed

+275
-0
lines changed

2 files changed

+275
-0
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ The proposals themselves are colloquially referred to as KEEPs.
1111
| Coroutines | repo: [kotlin-coroutines](https://github.com/Kotlin/kotlin-coroutines) | [Issues](https://github.com/Kotlin/kotlin-coroutines/issues)
1212
| Type aliases | [type-aliases.md](proposals/type-aliases.md) | [Issue #4](https://github.com/Kotlin/KEEP/issues/4)
1313
| Bound callable references | [bound-callable-references.md](proposals/bound-callable-references.md) | [Issue #5](https://github.com/Kotlin/KEEP/issues/5)
14+
| Use JavaScript modules from Kotlin | [js/consume-js-modules.md](proposals/js/consume-js-modules.md) | [PR #TODO](https://github.com/Kotlin/KEEP/pull/TODO)
1415

1516
## How to give feedback
1617

proposals/js/consume-js-modules.md

+274
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
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

Comments
 (0)