Skip to content

Commit 32fe220

Browse files
maximegirardetGitLab
authored andcommitted
merge: ES2015 & ESM support
See merge request opensavvy/automation/kotlin-vite!119
2 parents 1e7c1f6 + 93e6b82 commit 32fe220

File tree

15 files changed

+422
-8
lines changed

15 files changed

+422
-8
lines changed

.idea/runConfigurations/Build_the_multi_project_example.xml

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/runConfigurations/Build_the_simple_example.xml

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/runConfigurations/Run_the_multi_project_example.xml

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/runConfigurations/Run_the_simple_example.xml

Lines changed: 2 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/website/docs/guides/es2015.md

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,192 @@
1+
# Reducing KotlinJS bundle size using ES2015
2+
3+
The different versions of the JavaScript language are standardized via the EcmaScript standard. By default, Kotlin generates JavaScript code that follows the ES5 standard (2009). Since Kotlin 2.0, the compiler has been able to generate ES2015 code.
4+
5+
## Comparing ES5 and ES2015
6+
7+
By default, the Kotlin compiler emits anonymous objects to represent classes. However, in ES2015, the compiler is able to generate proper JavaScript classes.
8+
9+
=== "Kotlin"
10+
11+
```kotlin
12+
class Point(
13+
val x: Int,
14+
val y: Int,
15+
) {
16+
17+
override fun toString() = "Point($x, $y)"
18+
}
19+
```
20+
21+
=== "Default: ES5"
22+
23+
```javascript
24+
initMetadataForClass(Point, 'Point');
25+
// …
26+
function Point(x, y) {
27+
this.h_1 = x;
28+
this.i_1 = y;
29+
}
30+
protoOf(Point).toString = function () {
31+
return 'Point(' + this.h_1 + ', ' + this.i_1 + ')';
32+
};
33+
```
34+
35+
=== "ES2015"
36+
37+
```javascript
38+
class Point {
39+
constructor(x, y) {
40+
this.h_1 = x;
41+
this.i_1 = y;
42+
}
43+
toString() {
44+
return 'Point(' + this.h_1 + ', ' + this.i_1 + ')';
45+
}
46+
}
47+
```
48+
49+
While this doesn't change much in terms of bundle size, it does improve the development experience for mixed Kotlin/JavaScript projects.
50+
51+
ES2015 also adds support for lambda expressions.
52+
53+
More importantly, the Kotlin compiler is able to use ES2015 generator expressions to implement the `suspend` keyword, which is used by coroutines and sequences. To compare, here is a simple sequence generator:
54+
55+
=== "Kotlin"
56+
57+
```kotlin
58+
fun fibonacci() = sequence {
59+
var a = 0
60+
var b = 1
61+
while (true) {
62+
val tmp = a + b
63+
a = b
64+
b = tmp
65+
yield(tmp)
66+
}
67+
}
68+
```
69+
70+
=== "Default: ES5"
71+
72+
```javascript
73+
// …
74+
function fibonacci$slambda(resultContinuation) {
75+
CoroutineImpl.call(this, resultContinuation);
76+
}
77+
protoOf(fibonacci$slambda).c3 = function ($this$sequence, $completion) {
78+
var tmp = this.d3($this$sequence, $completion);
79+
tmp.d1_1 = Unit_instance;
80+
tmp.e1_1 = null;
81+
return tmp.j1();
82+
};
83+
protoOf(fibonacci$slambda).a2 = function (p1, $completion) {
84+
return this.c3(p1 instanceof SequenceScope ? p1 : THROW_CCE(), $completion);
85+
};
86+
protoOf(fibonacci$slambda).j1 = function () {
87+
var suspendResult = this.d1_1;
88+
$sm: do
89+
try {
90+
var tmp = this.b1_1;
91+
switch (tmp) {
92+
case 0:
93+
this.c1_1 = 3;
94+
this.z2_1 = 0;
95+
this.a3_1 = 1;
96+
this.b1_1 = 1;
97+
continue $sm;
98+
case 1:
99+
if (!true) {
100+
this.b1_1 = 4;
101+
continue $sm;
102+
}
103+
104+
this.b3_1 = this.z2_1 + this.a3_1 | 0;
105+
this.z2_1 = this.a3_1;
106+
this.a3_1 = this.b3_1;
107+
this.b1_1 = 2;
108+
suspendResult = this.y2_1.d2(this.b3_1, this);
109+
if (suspendResult === get_COROUTINE_SUSPENDED()) {
110+
return suspendResult;
111+
}
112+
113+
continue $sm;
114+
case 2:
115+
this.b1_1 = 1;
116+
continue $sm;
117+
case 3:
118+
throw this.e1_1;
119+
case 4:
120+
return Unit_instance;
121+
}
122+
} catch ($p) {
123+
var e = $p;
124+
if (this.c1_1 === 3) {
125+
throw e;
126+
} else {
127+
this.b1_1 = this.c1_1;
128+
this.e1_1 = e;
129+
}
130+
}
131+
while (true);
132+
};
133+
protoOf(fibonacci$slambda).d3 = function ($this$sequence, completion) {
134+
var i = new fibonacci$slambda(completion);
135+
i.y2_1 = $this$sequence;
136+
return i;
137+
};
138+
// …
139+
```
140+
141+
=== "ES2015"
142+
143+
```kotlin
144+
// …
145+
function *_generator_invoke__zhh2q8($this, $this$sequence, $completion) {
146+
var a = 0;
147+
var b = 1;
148+
while (true) {
149+
var tmp = a + b | 0;
150+
a = b;
151+
b = tmp;
152+
var tmp_0 = $this$sequence.a3(tmp, $completion);
153+
if (tmp_0 === get_COROUTINE_SUSPENDED())
154+
tmp_0 = yield tmp_0;
155+
}
156+
return Unit_instance;
157+
}
158+
function fibonacci$slambda_0() {
159+
var i = new fibonacci$slambda();
160+
var l = ($this$sequence, $completion) => i.n3($this$sequence, $completion);
161+
l.$arity = 1;
162+
return l;
163+
}
164+
// …
165+
```
166+
167+
In this small example, the final bundle is already smaller by 0.74 kB, over a single function.
168+
In coroutines-heavy applications, for example if you're using Ktor, this difference grows continuously.
169+
170+
## Configuration
171+
172+
!!! info ""
173+
If you haven't configured Vite for your project yet, start with the [initial setup](index.md).
174+
175+
To enable ES2015, configure the Kotlin compiler options. The Vite for Kotlin plugin will automatically pick it up.
176+
177+
```kotlin title="build.gradle.kts" hl_lines="8-10"
178+
//
179+
180+
kotlin {
181+
js {
182+
browser()
183+
binaries.executable()
184+
185+
compilerOptions {
186+
target.set("es2015")
187+
}
188+
}
189+
190+
//
191+
}
192+
```

docs/website/docs/guides/esm.md

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# Building a KotlinJS website with ESM (ES Modules)
2+
3+
When the Kotlin compiler generates JavaScript code, it must somehow link the different files together. In the JavaScript ecosystem, there are many ways of importing files within each other.
4+
5+
- AMD, UMD and CommonJS are different ways to import files. CommonJS is the name of Node.js' `require()` function.
6+
- ESM is the native `import` statement that is part of the JavaScript language itself.
7+
8+
By default, the Kotlin compiler uses UMD. However, UMD isn't natively supported by Vite, so Vite for Kotlin uses preprocessors to convert the UMD declarations to ESM, which is natively supported by Vite.
9+
10+
By instructing the Kotlin compiler to directly generate code using ESM imports, we remove the need for preprocessing the source files. This makes Vite even faster, especially during development auto-reload.
11+
12+
## Comparing the generated code
13+
14+
Because ESM is the native JavaScript format, the generated files are smaller. For example, compare the following two generated files for a simple hello world that prints a message to the console (comments and source maps have been removed):
15+
16+
=== "Default configuration"
17+
18+
```javascript hl_lines="15-17"
19+
(function (factory) {
20+
if (typeof define === 'function' && define.amd)
21+
define(['exports', './kotlin-kotlin-stdlib.js'], factory);
22+
else if (typeof exports === 'object')
23+
factory(module.exports, require('./kotlin-kotlin-stdlib.js'));
24+
else {
25+
if (typeof globalThis['kotlin-kotlin-stdlib'] === 'undefined') {
26+
throw new Error("Error loading module 'example-simple'. Its dependency 'kotlin-kotlin-stdlib' was not found. Please, check whether 'kotlin-kotlin-stdlib' is loaded prior to 'example-simple'.");
27+
}
28+
globalThis['example-simple'] = factory(typeof globalThis['example-simple'] === 'undefined' ? {} : globalThis['example-simple'], globalThis['kotlin-kotlin-stdlib']);
29+
}
30+
}(function (_, kotlin_kotlin) {
31+
'use strict';
32+
var println = kotlin_kotlin.$_$.a;
33+
function main() {
34+
println('Hello world!');
35+
}
36+
function mainWrapper() {
37+
main();
38+
}
39+
mainWrapper();
40+
return _;
41+
}));
42+
```
43+
44+
=== "Using ES Modules"
45+
46+
```javascript hl_lines="2-4"
47+
import { println2shhhgwwt4c61 as println } from './kotlin-kotlin-stdlib.mjs';
48+
function main() {
49+
println('Hello world!');
50+
}
51+
function mainWrapper() {
52+
main();
53+
}
54+
mainWrapper();
55+
```
56+
57+
On this example, the final bundle is 4.22 kB by default and 2.68 kB with ESM. This is because Vite's bundler, Rollup, is much better at detecting unused code when ESM imports are used.
58+
59+
## Configuration
60+
61+
!!! info ""
62+
If you haven't configured Vite for your project yet, start with the [initial setup](index.md).
63+
64+
The Vite for Kotlin plugin will automatically pick up the Kotlin's compiler configuration.
65+
66+
Start by instructing the Kotlin compiler to generate ESM modules:
67+
```kotlin title="build.gradle.kts" hl_lines="7"
68+
//
69+
70+
kotlin {
71+
js {
72+
browser()
73+
binaries.executable()
74+
useEsModules()
75+
}
76+
77+
//
78+
}
79+
```
80+
81+
The extension for ESM is `.mjs` instead of `.js`, so we must update our `index.html`:
82+
```html title="index.html"
83+
<!---->
84+
<script src="example-simple.mjs" type="module"></script>
85+
<!---->
86+
```
87+
Note the `.mjs` where the file previously contained a `.js`.

0 commit comments

Comments
 (0)