From e4452152b7511653b5e6bf8d2503a02d98a3ce78 Mon Sep 17 00:00:00 2001
From: Alexey Andreev <konsoletyper@gmail.com>
Date: Thu, 22 Feb 2024 21:14:30 +0100
Subject: [PATCH] jso: improve JS class import to Java

---
 .../java/net/impl/TXHRURLConnection.java      |   6 +-
 .../teavm/classlib/java/nio/TByteOrder.java   |   6 +-
 .../classlib/java/security/TSecureRandom.java |   2 +-
 .../org/teavm/classlib/java/util/TDate.java   |  22 +-
 .../org/teavm/jso/ajax/XMLHttpRequest.java    |  83 +++++---
 .../java/org/teavm/jso/browser/Navigator.java |  16 +-
 .../org/teavm/jso/browser/Performance.java    |   3 +-
 .../java/org/teavm/jso/canvas/ImageData.java  |  21 +-
 .../java/org/teavm/jso/canvas/Path2D.java     |  41 ++--
 .../main/java/org/teavm/jso/core/JSArray.java |  74 ++++---
 .../main/java/org/teavm/jso/core/JSDate.java  | 133 +++++++-----
 .../jso/core/JSFinalizationRegistry.java      |  11 +-
 .../main/java/org/teavm/jso/core/JSMap.java   |  18 +-
 .../java/org/teavm/jso/core/JSPromise.java    |  27 ++-
 .../java/org/teavm/jso/core/JSRegExp.java     |  28 ++-
 .../java/org/teavm/jso/core/JSString.java     |   1 -
 .../java/org/teavm/jso/core/JSSymbol.java     |   5 +-
 .../java/org/teavm/jso/core/JSWeakMap.java    |  16 +-
 .../java/org/teavm/jso/core/JSWeakRef.java    |   6 +
 .../main/java/org/teavm/jso/json/JSON.java    |   7 +-
 .../teavm/jso/typedarrays/ArrayBuffer.java    |  17 +-
 .../jso/typedarrays/ArrayBufferView.java      |  36 ++--
 .../org/teavm/jso/typedarrays/DataView.java   |  73 ++++---
 .../teavm/jso/typedarrays/Float32Array.java   |  30 ++-
 .../teavm/jso/typedarrays/Float64Array.java   |  30 ++-
 .../org/teavm/jso/typedarrays/Int16Array.java |  30 ++-
 .../org/teavm/jso/typedarrays/Int32Array.java |  36 +++-
 .../org/teavm/jso/typedarrays/Int8Array.java  |  30 ++-
 .../teavm/jso/typedarrays/Uint16Array.java    |  30 ++-
 .../org/teavm/jso/typedarrays/Uint8Array.java |  30 ++-
 .../jso/typedarrays/Uint8ClampedArray.java    |  29 ++-
 .../org/teavm/jso/webaudio/AudioContext.java  |  93 ++++-----
 .../jso/webaudio/OfflineAudioContext.java     |  14 +-
 .../org/teavm/jso/websocket/WebSocket.java    |  50 +++--
 .../org/teavm/jso/workers/SharedWorker.java   |  29 ++-
 .../java/org/teavm/jso/workers/Worker.java    |  34 +++-
 .../src/main/java/org/teavm/jso/JSModule.java |  27 +++
 .../src/main/java/org/teavm/jso/impl/JS.java  |  93 ++++++++-
 .../org/teavm/jso/impl/JSAliasRenderer.java   |   2 +-
 .../org/teavm/jso/impl/JSAnnotationCache.java | 128 ++++++++++++
 .../org/teavm/jso/impl/JSClassProcessor.java  | 189 ++++++++++++++----
 .../jso/impl/JSImportAnnotationCache.java     |  66 ++++++
 .../teavm/jso/impl/JSImportDescriptor.java    |  45 +++++
 .../java/org/teavm/jso/impl/JSImportKind.java |  22 ++
 .../java/org/teavm/jso/impl/JSMethods.java    |  15 +-
 .../org/teavm/jso/impl/JSNativeInjector.java  |  25 ++-
 .../jso/impl/JSObjectClassTransformer.java    |  18 ++
 .../java/org/teavm/jso/impl/JSTypeHelper.java |  14 +-
 .../org/teavm/jso/impl/JSValueMarshaller.java |  88 ++++++++
 .../java/org/teavm/jso/impl/JSWrapper.java    |  12 +-
 .../java/org/teavm/samples/hello/Client.java  |   2 +-
 .../teavm/samples/promise/PromiseExample.java |  28 +--
 .../samples/software3d/teavm/Controller.kt    |   6 +-
 .../org/teavm/samples/webapis/Storage.java    |   4 +-
 .../teavm/jso/test/ClassWithConstructor.java  |  36 ++++
 .../test/ClassWithConstructorInModule.java    |  36 ++++
 .../org/teavm/jso/test/ImportClassTest.java   |  18 ++
 .../org/teavm/jso/test/ImportModuleTest.java  |  12 ++
 .../teavm/jso/test/classWithConstructor.js    |  33 +++
 .../jso/test/classWithConstructorInModule.js  |  29 +++
 .../tooling/deobfuscate/js/Deobfuscator.java  |   6 +-
 61 files changed, 1612 insertions(+), 459 deletions(-)
 create mode 100644 jso/core/src/main/java/org/teavm/jso/JSModule.java
 create mode 100644 jso/impl/src/main/java/org/teavm/jso/impl/JSAnnotationCache.java
 create mode 100644 jso/impl/src/main/java/org/teavm/jso/impl/JSImportAnnotationCache.java
 create mode 100644 jso/impl/src/main/java/org/teavm/jso/impl/JSImportDescriptor.java
 create mode 100644 jso/impl/src/main/java/org/teavm/jso/impl/JSImportKind.java
 create mode 100644 tests/src/test/java/org/teavm/jso/test/ClassWithConstructor.java
 create mode 100644 tests/src/test/java/org/teavm/jso/test/ClassWithConstructorInModule.java
 create mode 100644 tests/src/test/resources/org/teavm/jso/test/classWithConstructor.js
 create mode 100644 tests/src/test/resources/org/teavm/jso/test/classWithConstructorInModule.js

diff --git a/classlib/src/main/java/org/teavm/classlib/java/net/impl/TXHRURLConnection.java b/classlib/src/main/java/org/teavm/classlib/java/net/impl/TXHRURLConnection.java
index ae408e2d4..7fffd9fcd 100644
--- a/classlib/src/main/java/org/teavm/classlib/java/net/impl/TXHRURLConnection.java
+++ b/classlib/src/main/java/org/teavm/classlib/java/net/impl/TXHRURLConnection.java
@@ -65,7 +65,7 @@ public class TXHRURLConnection extends THttpURLConnection {
             return;
         }
 
-        xhr = XMLHttpRequest.create();
+        xhr = new XMLHttpRequest();
         xhr.open(method, url.toString());
         for (Map.Entry<String, List<String>> entry : getRequestProperties().entrySet()) {
             for (String value : entry.getValue()) {
@@ -97,7 +97,7 @@ public class TXHRURLConnection extends THttpURLConnection {
                 responseCode = -1;
             }
 
-            Int8Array array = Int8Array.create((ArrayBuffer) xhr.getResponse());
+            var array = new Int8Array((ArrayBuffer) xhr.getResponse());
             byte[] bytes = new byte[array.getLength()];
             for (int i = 0; i < bytes.length; ++i) {
                 bytes[i] = array.get(i);
@@ -119,7 +119,7 @@ public class TXHRURLConnection extends THttpURLConnection {
 
         if (outputStream != null) {
             byte[] bytes = outputStream.toByteArray();
-            Int8Array array = Int8Array.create(bytes.length);
+            var array = new Int8Array(bytes.length);
             for (int i = 0; i < bytes.length; ++i) {
                 array.set(i, bytes[i]);
             }
diff --git a/classlib/src/main/java/org/teavm/classlib/java/nio/TByteOrder.java b/classlib/src/main/java/org/teavm/classlib/java/nio/TByteOrder.java
index 79ef0e5b4..bc3ca6a1d 100644
--- a/classlib/src/main/java/org/teavm/classlib/java/nio/TByteOrder.java
+++ b/classlib/src/main/java/org/teavm/classlib/java/nio/TByteOrder.java
@@ -34,10 +34,10 @@ public final class TByteOrder {
     public static TByteOrder nativeOrder() {
         if (nativeOrder == null) {
             if (PlatformDetector.isJavaScript()) {
-                var buffer = ArrayBuffer.create(2);
-                var shortArray = Int16Array.create(buffer);
+                var buffer = new ArrayBuffer(2);
+                var shortArray = new Int16Array(buffer);
                 shortArray.set(0, (short) 1);
-                var byteArray = Int8Array.create(buffer);
+                var byteArray = new Int8Array(buffer);
                 nativeOrder = byteArray.get(0) == 0 ? BIG_ENDIAN : LITTLE_ENDIAN;
             } else {
                 var array = new short[1];
diff --git a/classlib/src/main/java/org/teavm/classlib/java/security/TSecureRandom.java b/classlib/src/main/java/org/teavm/classlib/java/security/TSecureRandom.java
index 76d2f3f54..044854866 100644
--- a/classlib/src/main/java/org/teavm/classlib/java/security/TSecureRandom.java
+++ b/classlib/src/main/java/org/teavm/classlib/java/security/TSecureRandom.java
@@ -75,7 +75,7 @@ public class TSecureRandom extends TRandom {
     @Override
     public void nextBytes(byte[] bytes) {
         if (PlatformDetector.isJavaScript() && Crypto.isSupported()) {
-            Uint8Array buffer = Uint8Array.create(bytes.length);
+            var buffer = new Uint8Array(bytes.length);
             Crypto.current().getRandomValues(buffer);
 
             for (int i = 0; i < bytes.length; ++i) {
diff --git a/classlib/src/main/java/org/teavm/classlib/java/util/TDate.java b/classlib/src/main/java/org/teavm/classlib/java/util/TDate.java
index f2f3dd39a..5dfeaf5b3 100644
--- a/classlib/src/main/java/org/teavm/classlib/java/util/TDate.java
+++ b/classlib/src/main/java/org/teavm/classlib/java/util/TDate.java
@@ -64,7 +64,7 @@ public class TDate implements TComparable<TDate> {
     public TDate(int year, int month, int date, int hrs, int min, int sec) {
         this(PlatformDetector.isLowLevel()
                 ? initDateLowLevel(year, month, date, hrs, min, sec)
-                : (long) JSDate.create(year, month, date, hrs, min, sec).getTime());
+                : (long) new JSDate(year, month, date, hrs, min, sec).getTime());
         if (!PlatformDetector.isLowLevel()) {
             setYear(year);
         }
@@ -127,7 +127,7 @@ public class TDate implements TComparable<TDate> {
         if (PlatformDetector.isLowLevel()) {
             return getYearLowLevel(value);
         }
-        return JSDate.create(value).getFullYear() - 1900;
+        return new JSDate(value).getFullYear() - 1900;
     }
 
     @Import(name = "teavm_date_getYear")
@@ -143,7 +143,7 @@ public class TDate implements TComparable<TDate> {
             value = setYearLowLevel(value, year);
             return;
         }
-        JSDate date = JSDate.create(value);
+        var date = new JSDate(value);
         date.setFullYear(year + 1900);
         value = (long) date.getTime();
     }
@@ -160,7 +160,7 @@ public class TDate implements TComparable<TDate> {
         if (PlatformDetector.isLowLevel()) {
             return getMonthLowLevel(value);
         }
-        return JSDate.create(value).getMonth();
+        return new JSDate(value).getMonth();
     }
 
     @Import(name = "teavm_date_getMonth")
@@ -176,7 +176,7 @@ public class TDate implements TComparable<TDate> {
             value = setMonthLowLevel(value, month);
             return;
         }
-        JSDate date = JSDate.create(value);
+        var date = new JSDate(value);
         date.setMonth(month);
         value = (long) date.getTime();
     }
@@ -193,7 +193,7 @@ public class TDate implements TComparable<TDate> {
         if (PlatformDetector.isLowLevel()) {
             return getDateLowLevel(value);
         }
-        return JSDate.create(value).getDate();
+        return new JSDate(value).getDate();
     }
 
     @Import(name = "teavm_date_getDate")
@@ -209,7 +209,7 @@ public class TDate implements TComparable<TDate> {
             value = setDateLowLevel(value, date);
             return;
         }
-        JSDate d = JSDate.create(value);
+        var d = new JSDate(value);
         d.setDate(date);
         this.value = (long) d.getTime();
     }
@@ -226,7 +226,7 @@ public class TDate implements TComparable<TDate> {
         if (PlatformDetector.isLowLevel()) {
             return getDayLowLevel(value);
         }
-        return JSDate.create(value).getDay();
+        return new JSDate(value).getDay();
     }
 
     @Import(name = "teavm_date_getDay")
@@ -239,7 +239,7 @@ public class TDate implements TComparable<TDate> {
         if (PlatformDetector.isLowLevel()) {
             return getHoursLowLevel(value);
         }
-        return JSDate.create(value).getHours();
+        return new JSDate(value).getHours();
     }
 
     @Import(name = "teavm_date_getHours")
@@ -389,12 +389,12 @@ public class TDate implements TComparable<TDate> {
 
     @Deprecated
     public String toLocaleString() {
-        return JSDate.create(value).toLocaleFormat("%c");
+        return new JSDate(value).toLocaleFormat("%c");
     }
 
     @Deprecated
     public String toGMTString() {
-        return JSDate.create(value).toUTCString();
+        return new JSDate(value).toUTCString();
     }
 
     @Deprecated
diff --git a/jso/apis/src/main/java/org/teavm/jso/ajax/XMLHttpRequest.java b/jso/apis/src/main/java/org/teavm/jso/ajax/XMLHttpRequest.java
index bcecfa1b9..345f61c4c 100644
--- a/jso/apis/src/main/java/org/teavm/jso/ajax/XMLHttpRequest.java
+++ b/jso/apis/src/main/java/org/teavm/jso/ajax/XMLHttpRequest.java
@@ -16,6 +16,7 @@
 package org.teavm.jso.ajax;
 
 import org.teavm.jso.JSBody;
+import org.teavm.jso.JSClass;
 import org.teavm.jso.JSObject;
 import org.teavm.jso.JSProperty;
 import org.teavm.jso.dom.events.Event;
@@ -23,7 +24,8 @@ import org.teavm.jso.dom.events.EventListener;
 import org.teavm.jso.dom.events.EventTarget;
 import org.teavm.jso.dom.xml.Document;
 
-public abstract class XMLHttpRequest implements JSObject, EventTarget {
+@JSClass
+public class XMLHttpRequest implements JSObject, EventTarget {
     public static final int UNSET = 0;
 
     public static final int OPENED = 1;
@@ -34,52 +36,55 @@ public abstract class XMLHttpRequest implements JSObject, EventTarget {
 
     public static final int DONE = 4;
 
-    public abstract void open(String method, String url);
+    public XMLHttpRequest() {
+    }
 
-    public abstract void open(String method, String url, boolean async);
+    public native void open(String method, String url);
 
-    public abstract void open(String method, String url, boolean async, String user);
+    public native void open(String method, String url, boolean async);
 
-    public abstract void open(String method, String url, boolean async, String user, String password);
+    public native void open(String method, String url, boolean async, String user);
 
-    public abstract void send();
+    public native void open(String method, String url, boolean async, String user, String password);
 
-    public abstract void send(String data);
+    public native void send();
 
-    public abstract void send(JSObject data);
+    public native void send(String data);
 
-    public abstract void setRequestHeader(String name, String value);
+    public native void send(JSObject data);
 
-    public abstract String getAllResponseHeaders();
+    public native void setRequestHeader(String name, String value);
 
-    public abstract String getResponseHeader(String name);
+    public native String getAllResponseHeaders();
+
+    public native String getResponseHeader(String name);
 
     @JSProperty("onreadystatechange")
-    public abstract void setOnReadyStateChange(ReadyStateChangeHandler handler);
+    public native void setOnReadyStateChange(ReadyStateChangeHandler handler);
 
     @JSProperty("onreadystatechange")
-    public abstract void setOnReadyStateChange(EventListener<Event> handler);
+    public native void setOnReadyStateChange(EventListener<Event> handler);
 
     @JSProperty("onabort")
-    public abstract void onAbort(EventListener<ProgressEvent> eventListener);
+    public native void onAbort(EventListener<ProgressEvent> eventListener);
 
     @JSProperty("onerror")
-    public abstract void onError(EventListener<ProgressEvent> eventListener);
+    public native void onError(EventListener<ProgressEvent> eventListener);
 
     @JSProperty("onload")
-    public abstract void onLoad(EventListener<ProgressEvent> eventListener);
+    public native void onLoad(EventListener<ProgressEvent> eventListener);
 
     @JSProperty("onloadstart")
-    public abstract void onLoadStart(EventListener<ProgressEvent> eventListener);
+    public native void onLoadStart(EventListener<ProgressEvent> eventListener);
 
     @JSProperty("onloadend")
-    public abstract void onLoadEnd(EventListener<ProgressEvent> eventListener);
+    public native void onLoadEnd(EventListener<ProgressEvent> eventListener);
 
     @JSProperty("onprogress")
-    public abstract void onProgress(EventListener<ProgressEvent> eventListener);
+    public native void onProgress(EventListener<ProgressEvent> eventListener);
 
     @JSProperty("ontimeout")
-    public abstract void onTimeout(EventListener<ProgressEvent> eventListener);
+    public native void onTimeout(EventListener<ProgressEvent> eventListener);
 
     public final void onComplete(Runnable runnable) {
         setOnReadyStateChange(() -> {
@@ -89,37 +94,53 @@ public abstract class XMLHttpRequest implements JSObject, EventTarget {
         });
     }
 
-    public abstract void overrideMimeType(String mimeType);
+    public native void overrideMimeType(String mimeType);
 
     @JSProperty
-    public abstract int getReadyState();
+    public native int getReadyState();
 
     @JSProperty
-    public abstract String getResponseText();
+    public native String getResponseText();
 
     @JSProperty
-    public abstract Document getResponseXML();
+    public native Document getResponseXML();
 
     @JSProperty
-    public abstract JSObject getResponse();
+    public native JSObject getResponse();
 
     @JSProperty
-    public abstract int getStatus();
+    public native int getStatus();
 
     @JSProperty
-    public abstract String getStatusText();
+    public native String getStatusText();
 
     @JSProperty
-    public abstract void setResponseType(String type);
+    public native void setResponseType(String type);
 
     @JSProperty
-    public abstract String getResponseType();
+    public native String getResponseType();
 
     @JSBody(script = "return new XMLHttpRequest();")
+    @Deprecated
     public static native XMLHttpRequest create();
 
-    public abstract void abort();
+    public native void abort();
 
     @JSProperty
-    public abstract String getResponseURL();
+    public native String getResponseURL();
+
+    @Override
+    public native void addEventListener(String type, EventListener<?> listener, boolean useCapture);
+
+    @Override
+    public native void addEventListener(String type, EventListener<?> listener);
+
+    @Override
+    public native void removeEventListener(String type, EventListener<?> listener, boolean useCapture);
+
+    @Override
+    public native void removeEventListener(String type, EventListener<?> listener);
+
+    @Override
+    public native boolean dispatchEvent(Event evt);
 }
diff --git a/jso/apis/src/main/java/org/teavm/jso/browser/Navigator.java b/jso/apis/src/main/java/org/teavm/jso/browser/Navigator.java
index 4dfb4d99e..af955cb8d 100644
--- a/jso/apis/src/main/java/org/teavm/jso/browser/Navigator.java
+++ b/jso/apis/src/main/java/org/teavm/jso/browser/Navigator.java
@@ -16,29 +16,33 @@
 package org.teavm.jso.browser;
 
 import org.teavm.jso.JSBody;
+import org.teavm.jso.JSClass;
+import org.teavm.jso.JSObject;
+import org.teavm.jso.JSProperty;
 import org.teavm.jso.gamepad.Gamepad;
 import org.teavm.jso.geolocation.Geolocation;
 
-public final class Navigator {
+@JSClass(name = "navigator")
+public final class Navigator implements JSObject {
     private Navigator() {
     }
 
-    @JSBody(script = "return navigator.onLine;")
+    @JSProperty("onLine")
     public static native boolean isOnline();
 
-    @JSBody(script = "return navigator.geolocation;")
+    @JSProperty
     public static native Geolocation getGeolocation();
 
     @JSBody(script = "return (\"geolocation\" in navigator);")
     public static native boolean isGeolocationAvailable();
 
-    @JSBody(script = "return navigator.language;")
+    @JSProperty
     public static native String getLanguage();
 
-    @JSBody(script = "return navigator.languages;")
+    @JSProperty
     public static native String[] getLanguages();
     
-    @JSBody(script = "return navigator.getGamepads();")
+    @JSProperty
     public static native Gamepad[] getGamepads();
 
     @JSBody(script = "return navigator.hardwareConcurrency")
diff --git a/jso/apis/src/main/java/org/teavm/jso/browser/Performance.java b/jso/apis/src/main/java/org/teavm/jso/browser/Performance.java
index a67c5c10e..6eeb23223 100644
--- a/jso/apis/src/main/java/org/teavm/jso/browser/Performance.java
+++ b/jso/apis/src/main/java/org/teavm/jso/browser/Performance.java
@@ -16,13 +16,14 @@
 package org.teavm.jso.browser;
 
 import org.teavm.jso.JSBody;
+import org.teavm.jso.JSClass;
 import org.teavm.jso.JSObject;
 
+@JSClass(name = "performance")
 public final class Performance implements JSObject {
     private Performance() {
     }
 
-    @JSBody(script = "return performance.now();")
     public static native double now();
 
     @JSBody(script = "return typeof(performance) !== 'undefined';")
diff --git a/jso/apis/src/main/java/org/teavm/jso/canvas/ImageData.java b/jso/apis/src/main/java/org/teavm/jso/canvas/ImageData.java
index bd4a68df3..26b11a64b 100644
--- a/jso/apis/src/main/java/org/teavm/jso/canvas/ImageData.java
+++ b/jso/apis/src/main/java/org/teavm/jso/canvas/ImageData.java
@@ -16,29 +16,40 @@
 package org.teavm.jso.canvas;
 
 import org.teavm.jso.JSBody;
+import org.teavm.jso.JSClass;
 import org.teavm.jso.JSObject;
 import org.teavm.jso.JSProperty;
 import org.teavm.jso.typedarrays.Uint8ClampedArray;
 
-public abstract class ImageData implements JSObject {
-    private ImageData() {
+@JSClass
+public class ImageData implements JSObject {
+    public ImageData(Uint8ClampedArray array, int width) {
+    }
+
+    public ImageData(int width, int height) {
+    }
+
+    public ImageData(Uint8ClampedArray array, int width, int height) {
     }
 
     @JSProperty
-    public abstract int getWidth();
+    public native int getWidth();
 
     @JSProperty
-    public abstract int getHeight();
+    public native int getHeight();
 
     @JSProperty
-    public abstract Uint8ClampedArray getData();
+    public native Uint8ClampedArray getData();
 
     @JSBody(params = { "array", "width" }, script = "return new ImageData(array, width);")
+    @Deprecated
     public static native ImageData create(Uint8ClampedArray array, int width);
 
     @JSBody(params = { "width", "height" }, script = "return new ImageData(width, height);")
+    @Deprecated
     public static native ImageData create(int width, int height);
 
     @JSBody(params = { "array", "width", "height" }, script = "return new ImageData(array, width, height);")
+    @Deprecated
     public static native ImageData create(Uint8ClampedArray array, int width, int height);
 }
diff --git a/jso/apis/src/main/java/org/teavm/jso/canvas/Path2D.java b/jso/apis/src/main/java/org/teavm/jso/canvas/Path2D.java
index bb9ed9d18..bfc230897 100644
--- a/jso/apis/src/main/java/org/teavm/jso/canvas/Path2D.java
+++ b/jso/apis/src/main/java/org/teavm/jso/canvas/Path2D.java
@@ -17,50 +17,61 @@ package org.teavm.jso.canvas;
 
 import org.teavm.interop.NoSideEffects;
 import org.teavm.jso.JSBody;
+import org.teavm.jso.JSClass;
 import org.teavm.jso.JSObject;
 
-public abstract class Path2D implements JSObject {
-    private Path2D() {
+@JSClass
+public class Path2D implements JSObject {
+    public Path2D() {
+    }
+
+    public Path2D(Path2D path) {
+    }
+
+    public Path2D(String svg) {
     }
 
     @JSBody(script = "return new Path2D();")
     @NoSideEffects
+    @Deprecated
     public static native Path2D create();
 
     @JSBody(params = "path", script = "return new Path2D(path);")
     @NoSideEffects
+    @Deprecated
     public static native Path2D create(Path2D path);
 
     @JSBody(params = "svg", script = "return new Path2D(svg);")
     @NoSideEffects
+    @Deprecated
     public static native Path2D create(String svg);
 
-    public abstract void addPath(Path2D path);
+    public native void addPath(Path2D path);
 
-    public abstract void closePath();
+    public native void closePath();
 
-    public abstract void moveTo(double x, double y);
+    public native void moveTo(double x, double y);
 
-    public abstract void lineTo(double x, double y);
+    public native void lineTo(double x, double y);
 
-    public abstract void bezierCurveTo(double cp1x, double cp1y, double cp2x, double cp2y, double x, double y);
+    public native void bezierCurveTo(double cp1x, double cp1y, double cp2x, double cp2y, double x, double y);
 
-    public abstract void quadraticCurveTo(double cpx, double cpy, double x, double y);
+    public native void quadraticCurveTo(double cpx, double cpy, double x, double y);
 
-    public abstract void arc(double x, double y, double radius, double startAngle, double endAngle);
+    public native void arc(double x, double y, double radius, double startAngle, double endAngle);
 
-    public abstract void arc(double x, double y, double radius, double startAngle, double endAngle,
+    public native void arc(double x, double y, double radius, double startAngle, double endAngle,
             boolean counterclockwise);
 
-    public abstract void arcTo(double x1, double y1, double x2, double y2, double radius);
+    public native void arcTo(double x1, double y1, double x2, double y2, double radius);
 
-    public abstract void ellipse(double x, double y, double radiusX, double radiusY, double rotation,
+    public native void ellipse(double x, double y, double radiusX, double radiusY, double rotation,
             double startAngle, double endAngle);
 
-    public abstract void ellipse(double x, double y, double radiusX, double radiusY, double rotation,
+    public native void ellipse(double x, double y, double radiusX, double radiusY, double rotation,
             double startAngle, double endAngle, boolean counterclockwise);
 
-    public abstract void rect(double x, double y, double width, double height);
+    public native void rect(double x, double y, double width, double height);
 
-    public abstract void roundRect(double x, double y, double width, double height, JSObject radii);
+    public native void roundRect(double x, double y, double width, double height, JSObject radii);
 }
diff --git a/jso/apis/src/main/java/org/teavm/jso/core/JSArray.java b/jso/apis/src/main/java/org/teavm/jso/core/JSArray.java
index 1ea7c5e51..7671383ee 100644
--- a/jso/apis/src/main/java/org/teavm/jso/core/JSArray.java
+++ b/jso/apis/src/main/java/org/teavm/jso/core/JSArray.java
@@ -17,80 +17,92 @@ package org.teavm.jso.core;
 
 import org.teavm.interop.NoSideEffects;
 import org.teavm.jso.JSBody;
+import org.teavm.jso.JSClass;
 import org.teavm.jso.JSIndexer;
 import org.teavm.jso.JSProperty;
 
-public abstract class JSArray<T> implements JSArrayReader<T> {
-    private JSArray() {
+@JSClass(name = "Array")
+public class JSArray<T> implements JSArrayReader<T> {
+    public JSArray(int size) {
+    }
+
+    public JSArray() {
     }
 
     @JSIndexer
-    public abstract void set(int index, T value);
+    public native void set(int index, T value);
 
-    public abstract int push(T a);
+    public native int push(T a);
 
-    public abstract int push(T a, T b);
+    public native int push(T a, T b);
 
-    public abstract int push(T a, T b, T c);
+    public native int push(T a, T b, T c);
 
-    public abstract int push(T a, T b, T c, T d);
+    public native int push(T a, T b, T c, T d);
 
-    public abstract T shift();
+    public native T shift();
 
-    public abstract String join(String separator);
+    public native String join(String separator);
 
-    public abstract String join();
+    public native String join();
 
-    public abstract JSArray<T> concat(JSArrayReader<T> a);
+    public native JSArray<T> concat(JSArrayReader<T> a);
 
-    public abstract JSArray<T> concat(JSArrayReader<T> a, JSArrayReader<T> b);
+    public native JSArray<T> concat(JSArrayReader<T> a, JSArrayReader<T> b);
 
-    public abstract JSArray<T> concat(JSArrayReader<T> a, JSArrayReader<T> b, JSArrayReader<T> c);
+    public native JSArray<T> concat(JSArrayReader<T> a, JSArrayReader<T> b, JSArrayReader<T> c);
 
-    public abstract JSArray<T> concat(JSArrayReader<T> a, JSArrayReader<T> b, JSArrayReader<T> c, JSArrayReader<T> d);
+    public native JSArray<T> concat(JSArrayReader<T> a, JSArrayReader<T> b, JSArrayReader<T> c, JSArrayReader<T> d);
 
-    public abstract T pop();
+    public native T pop();
 
-    public abstract int unshift(T a);
+    public native int unshift(T a);
 
-    public abstract int unshift(T a, T b);
+    public native int unshift(T a, T b);
 
-    public abstract int unshift(T a, T b, T c);
+    public native int unshift(T a, T b, T c);
 
-    public abstract int unshift(T a, T b, T c, T d);
+    public native int unshift(T a, T b, T c, T d);
 
-    public abstract JSArray<T> slice(int start);
+    public native JSArray<T> slice(int start);
 
-    public abstract JSArray<T> slice(int start, int end);
+    public native JSArray<T> slice(int start, int end);
 
-    public abstract JSArray<T> reverse();
+    public native JSArray<T> reverse();
 
-    public abstract JSArray<T> sort(JSSortFunction<T> function);
+    public native JSArray<T> sort(JSSortFunction<T> function);
 
-    public abstract JSArray<T> sort();
+    public native JSArray<T> sort();
 
-    public abstract JSArray<T> splice(int start, int count);
+    public native JSArray<T> splice(int start, int count);
 
-    public abstract JSArray<T> splice(int start, int count, T a);
+    public native JSArray<T> splice(int start, int count, T a);
 
-    public abstract JSArray<T> splice(int start, int count, T a, T b);
+    public native JSArray<T> splice(int start, int count, T a, T b);
 
-    public abstract JSArray<T> splice(int start, int count, T a, T b, T c);
+    public native JSArray<T> splice(int start, int count, T a, T b, T c);
 
-    public abstract JSArray<T> splice(int start, int count, T a, T b, T c, T d);
+    public native JSArray<T> splice(int start, int count, T a, T b, T c, T d);
     
     @JSProperty
-    public abstract void setLength(int len);
+    public native void setLength(int len);
+
+    @Override
+    public native int getLength();
+
+    @Override
+    public native T get(int index);
 
     @JSBody(script = "return new Array();")
     @NoSideEffects
+    @Deprecated
     public static native <T> JSArray<T> create();
 
     @JSBody(params = "size", script = "return new Array(size);")
     @NoSideEffects
+    @Deprecated
     public static native <T> JSArray<T> create(int size);
 
-    @JSBody(params = "object", script = "return Array.isArray(object);")
     @NoSideEffects
     public static native boolean isArray(Object object);
 
diff --git a/jso/apis/src/main/java/org/teavm/jso/core/JSDate.java b/jso/apis/src/main/java/org/teavm/jso/core/JSDate.java
index ee7069e99..e94967d70 100644
--- a/jso/apis/src/main/java/org/teavm/jso/core/JSDate.java
+++ b/jso/apis/src/main/java/org/teavm/jso/core/JSDate.java
@@ -17,164 +17,187 @@ package org.teavm.jso.core;
 
 import org.teavm.interop.NoSideEffects;
 import org.teavm.jso.JSBody;
+import org.teavm.jso.JSClass;
 import org.teavm.jso.JSMethod;
 import org.teavm.jso.JSObject;
 
-public abstract class JSDate implements JSObject {
+@JSClass(name = "Date")
+public class JSDate implements JSObject {
+    public JSDate() {
+    }
+
+    public JSDate(double millis) {
+    }
+
+    public JSDate(int year, int month) {
+    }
+
+    public JSDate(int year, int month, int day) {
+    }
+
+    public JSDate(int year, int month, int day, int hour) {
+    }
+
+    public JSDate(int year, int month, int day, int hour, int minute) {
+    }
+
+    public JSDate(int year, int month, int day, int hour, int minute, int second) {
+    }
+
+    public JSDate(int year, int month, int day, int hour, int minute, int second, int millisecond) {
+    }
+
     @JSBody(script = "return new Date();")
     @NoSideEffects
+    @Deprecated
     public static native JSDate create();
 
     @JSBody(params = "millis", script = "return new Date(millis);")
     @NoSideEffects
+    @Deprecated
     public static native JSDate create(double millis);
 
     @JSBody(params = { "year", "month" }, script = "return new Date(year, month);")
     @NoSideEffects
+    @Deprecated
     public static native JSDate create(int year, int month);
 
     @JSBody(params = { "year", "month", "day" }, script = "return new Date(year, month, day);")
     @NoSideEffects
+    @Deprecated
     public static native JSDate create(int year, int month, int day);
 
     @JSBody(params = { "year", "month", "day", "hour" }, script = "return new Date(year, month, day, hour);")
     @NoSideEffects
+    @Deprecated
     public static native JSDate create(int year, int month, int day, int hour);
 
     @JSBody(params = { "year", "month", "day", "hour", "minute" },
             script = "return new Date(year, month, day, hour, minute);")
     @NoSideEffects
+    @Deprecated
     public static native JSDate create(int year, int month, int day, int hour, int minute);
 
     @JSBody(params = { "year", "month", "day", "hour", "minute", "second" },
             script = "return new Date(year, month, day, hour, minute, second);")
     @NoSideEffects
+    @Deprecated
     public static native JSDate create(int year, int month, int day, int hour, int minute, int second);
 
     @JSBody(params = { "year", "month", "day", "hour", "minute", "second", "millisecond" },
             script = "return new Date(year, month, day, hour, minute, second, millisecond);")
     @NoSideEffects
+    @Deprecated
     public static native JSDate create(int year, int month, int day, int hour, int minute, int second, int millisecond);
 
-    @JSBody(params = {}, script = "return Date.now();")
     @NoSideEffects
     public static native double now();
 
-    @JSBody(params = "stringValue", script = "return Date.parse(stringValue);")
     @NoSideEffects
     public static native double parse(String stringValue);
 
-    @JSBody(params = { "year", "month" }, script = "return Date.UTC(year, month);")
     @NoSideEffects
     public static native double UTC(int year, int month);
 
-    @JSBody(params = { "year", "month", "day" }, script = "return Date.UTC(year, month, day);")
     @NoSideEffects
     public static native double UTC(int year, int month, int day);
 
-    @JSBody(params = { "year", "month", "day", "hour" }, script = "return Date.UTC(year, month, day, hour);")
     @NoSideEffects
     public static native double UTC(int year, int month, int day, int hour);
 
-    @JSBody(params = { "year", "month", "day", "hour", "minute" },
-            script = "return Date.UTC(year, month, day, hour, minute);")
     @NoSideEffects
     public static native double UTC(int year, int month, int day, int hour, int minute);
 
-    @JSBody(params = { "year", "month", "day", "hour", "minute", "second" },
-            script = "return Date.UTC(year, month, day, hour, minute, second);")
     @NoSideEffects
     public static native double UTC(int year, int month, int day, int hour, int minute, int second);
 
-    @JSBody(params = { "year", "month", "day", "hour", "minute", "second", "millisecond" },
-            script = "return Date.UTC(year, month, day, hour, minute, second, millisecond);")
     @NoSideEffects
     public static native double UTC(int year, int month, int day, int hour, int minute, int second, int millisecond);
 
-    public abstract int getDate();
+    public native int getDate();
 
-    public abstract int getDay();
+    public native int getDay();
 
-    public abstract int getFullYear();
+    public native int getFullYear();
 
-    public abstract int getHours();
+    public native int getHours();
 
-    public abstract int getMilliseconds();
+    public native int getMilliseconds();
 
-    public abstract int getMinutes();
+    public native int getMinutes();
 
-    public abstract int getMonth();
+    public native int getMonth();
 
-    public abstract int getSeconds();
+    public native int getSeconds();
 
-    public abstract double getTime();
+    public native double getTime();
 
-    public abstract int getTimezoneOffset();
+    public native int getTimezoneOffset();
 
-    public abstract int getUTCDate();
+    public native int getUTCDate();
 
-    public abstract int getUTCDay();
+    public native int getUTCDay();
 
-    public abstract int getUTCFullYear();
+    public native int getUTCFullYear();
 
-    public abstract int getUTCHours();
+    public native int getUTCHours();
 
-    public abstract int getUTCMilliseconds();
+    public native int getUTCMilliseconds();
 
-    public abstract int getUTCMinutes();
+    public native int getUTCMinutes();
 
-    public abstract int getUTCMonth();
+    public native int getUTCMonth();
 
-    public abstract int getUTCSeconds();
+    public native int getUTCSeconds();
 
-    public abstract void setDate(int date);
+    public native void setDate(int date);
 
-    public abstract void setFullYear(int fullYear);
+    public native void setFullYear(int fullYear);
 
-    public abstract void setHours(int hours);
+    public native void setHours(int hours);
 
-    public abstract void setMilliseconds(int milliseconds);
+    public native void setMilliseconds(int milliseconds);
 
-    public abstract void setMinutes(int minutes);
+    public native void setMinutes(int minutes);
 
-    public abstract void setMonth(int month);
+    public native void setMonth(int month);
 
-    public abstract void setSeconds(int seconds);
+    public native void setSeconds(int seconds);
 
-    public abstract void setTime(double time);
+    public native void setTime(double time);
 
-    public abstract void setUTCDate(int date);
+    public native void setUTCDate(int date);
 
-    public abstract void setUTCFullYear(int fullYear);
+    public native void setUTCFullYear(int fullYear);
 
-    public abstract void setUTCHours(int hours);
+    public native void setUTCHours(int hours);
 
-    public abstract void setUTCMilliseconds(int milliseconds);
+    public native void setUTCMilliseconds(int milliseconds);
 
-    public abstract void setUTCMinutes(int minutes);
+    public native void setUTCMinutes(int minutes);
 
-    public abstract void setUTCMonth(int month);
+    public native void setUTCMonth(int month);
 
-    public abstract void setUTCSeconds(int seconds);
+    public native void setUTCSeconds(int seconds);
 
-    public abstract String toDateString();
+    public native String toDateString();
 
-    public abstract String toISOString();
+    public native String toISOString();
 
-    public abstract String toJSON();
+    public native String toJSON();
 
-    public abstract String toLocaleDateString();
+    public native String toLocaleDateString();
 
-    public abstract String toLocaleString();
+    public native String toLocaleString();
 
-    public abstract String toLocaleTimeString();
+    public native String toLocaleTimeString();
 
     @JSMethod("toString")
-    public abstract String stringValue();
+    public native String stringValue();
 
-    public abstract String toTimeString();
+    public native String toTimeString();
 
-    public abstract String toUTCString();
+    public native String toUTCString();
 
-    public abstract String toLocaleFormat(String format);
+    public native String toLocaleFormat(String format);
 }
diff --git a/jso/apis/src/main/java/org/teavm/jso/core/JSFinalizationRegistry.java b/jso/apis/src/main/java/org/teavm/jso/core/JSFinalizationRegistry.java
index 53eeca28a..9129713f8 100644
--- a/jso/apis/src/main/java/org/teavm/jso/core/JSFinalizationRegistry.java
+++ b/jso/apis/src/main/java/org/teavm/jso/core/JSFinalizationRegistry.java
@@ -17,15 +17,20 @@ package org.teavm.jso.core;
 
 import org.teavm.interop.NoSideEffects;
 import org.teavm.jso.JSBody;
+import org.teavm.jso.JSClass;
 import org.teavm.jso.JSObject;
 
-public abstract class JSFinalizationRegistry implements JSObject {
-    public abstract void register(Object obj, Object token);
+@JSClass(name = "FinalizationRegistry")
+public class JSFinalizationRegistry implements JSObject {
+    public JSFinalizationRegistry(JSFinalizationRegistryConsumer consumer) {
+    }
+
+    public native void register(Object obj, Object token);
 
     @JSBody(params = "consumer", script = "return new FinalizationRegistry(consumer);")
+    @Deprecated
     public static native JSFinalizationRegistry create(JSFinalizationRegistryConsumer consumer);
 
-
     @JSBody(script = "return typeof FinalizationRegistry !== 'undefined';")
     @NoSideEffects
     public static native boolean isSupported();
diff --git a/jso/apis/src/main/java/org/teavm/jso/core/JSMap.java b/jso/apis/src/main/java/org/teavm/jso/core/JSMap.java
index bda5ecb15..43fd49ad9 100644
--- a/jso/apis/src/main/java/org/teavm/jso/core/JSMap.java
+++ b/jso/apis/src/main/java/org/teavm/jso/core/JSMap.java
@@ -17,20 +17,26 @@ package org.teavm.jso.core;
 
 import org.teavm.interop.NoSideEffects;
 import org.teavm.jso.JSBody;
+import org.teavm.jso.JSClass;
 import org.teavm.jso.JSObject;
 
-public abstract class JSMap<K extends JSObject, V extends JSObject> implements JSObject {
-    public abstract V get(K key);
+@JSClass(name = "Map")
+public class JSMap<K extends JSObject, V extends JSObject> implements JSObject {
+    public JSMap() {
+    }
 
-    public abstract boolean has(K key);
+    public native V get(K key);
 
-    public abstract JSMap<K, V> set(K key, V value);
+    public native boolean has(K key);
 
-    public abstract boolean delete(K key);
+    public native JSMap<K, V> set(K key, V value);
 
-    public abstract void clear();
+    public native boolean delete(K key);
+
+    public native void clear();
 
     @JSBody(script = "return new Map();")
     @NoSideEffects
+    @Deprecated
     public static native <K extends JSObject, V extends JSObject> JSMap<K, V> create();
 }
diff --git a/jso/apis/src/main/java/org/teavm/jso/core/JSPromise.java b/jso/apis/src/main/java/org/teavm/jso/core/JSPromise.java
index 5a3d9890a..a4f2ba00e 100644
--- a/jso/apis/src/main/java/org/teavm/jso/core/JSPromise.java
+++ b/jso/apis/src/main/java/org/teavm/jso/core/JSPromise.java
@@ -17,6 +17,7 @@ package org.teavm.jso.core;
 
 import org.teavm.interop.NoSideEffects;
 import org.teavm.jso.JSBody;
+import org.teavm.jso.JSClass;
 import org.teavm.jso.JSFunctor;
 import org.teavm.jso.JSMethod;
 import org.teavm.jso.JSObject;
@@ -32,8 +33,9 @@ import org.teavm.jso.util.function.JSSupplier;
  *
  * @param <T> The type this promise returns when resolving successfully.
  */
-public abstract class JSPromise<T> implements JSObject {
-    private JSPromise() {
+@JSClass(name = "Promise")
+public class JSPromise<T> implements JSObject {
+    public JSPromise(Executor<T> executor) {
     }
 
     /** Interface for a function wrapped by a promise. */
@@ -49,63 +51,58 @@ public abstract class JSPromise<T> implements JSObject {
 
     @JSBody(params = "executor", script = "return new Promise(executor);")
     @NoSideEffects
+    @Deprecated
     public static native <T> JSPromise<T> create(Executor<T> executor);
 
-    @JSBody(params = "promises", script = "return Promise.any(promises);")
     @NoSideEffects
     public static native <V> JSPromise<V> any(JSArrayReader<JSPromise<V>> promises);
 
     // TODO: Allow passing differently typed promises via a JSTuple<T1, ...> interface
-    @JSBody(params = "promises", script = "return Promise.all(promises);")
     @NoSideEffects
     public static native <V> JSPromise<JSArrayReader<V>> all(JSArrayReader<JSPromise<V>> promises);
 
     // TODO: Allow passing differently typed promises via a JSTuple<T1, ...> interface
-    @JSBody(params = "promises", script = "return Promise.allSettled(promises);")
     @NoSideEffects
     public static native <V> JSPromise<JSArrayReader<FulfillmentValue<V>>>
         allSettled(JSArrayReader<JSPromise<V>> promises);
 
-    @JSBody(params = "promises", script = "return Promise.race(promises);")
     @NoSideEffects
     public static native <V> JSPromise<V> race(JSArrayReader<JSPromise<V>> promises);
 
-    @JSBody(params = "value", script = "return Promise.resolve(value);")
     @NoSideEffects
     public static native <V> JSPromise<V> resolve(V value);
 
-    @JSBody(params = "reason", script = "return Promise.reject(reason);")
     @NoSideEffects
     public static native <V> JSPromise<V> reject(Object reason);
 
     /** Call {@code onFulfilled} with the success value, resolving with its return value. */
-    public abstract <V> JSPromise<V> then(JSFunction<T, V> onFulfilled);
+    public native <V> JSPromise<V> then(JSFunction<T, V> onFulfilled);
 
     /** Call {@code onFulfilled} with the success value or {@code onRejected} with the reject reason,
      *  resolving with its return value. */
-    public abstract <V> JSPromise<V> then(JSFunction<T, V> onFulfilled, JSFunction<Object, V> onRejected);
+    public native <V> JSPromise<V> then(JSFunction<T, V> onFulfilled, JSFunction<Object, V> onRejected);
 
     /** Call {@code onFulfilled} with the success value, returning a new promise. */
     @JSMethod("then")
-    public abstract <V> JSPromise<V> flatThen(JSFunction<T, ? extends JSPromise<V>> onFulfilled);
+    public native <V> JSPromise<V> flatThen(JSFunction<T, ? extends JSPromise<V>> onFulfilled);
 
     /** Call {@code onFulfilled} with the success value or {@code onRejected} with the reject reason,
      *  returning a new promise. */
     @JSMethod("then")
-    public abstract <V> JSPromise<V> flatThen(JSFunction<T, ? extends JSPromise<V>> onFulfilled,
+    public native <V> JSPromise<V> flatThen(JSFunction<T, ? extends JSPromise<V>> onFulfilled,
                                               JSFunction<Object, ? extends JSPromise<V>> onRejected);
 
     /** Call {@code onRejected} with the reject reason, resolving with its return value. */
     @JSMethod("catch")
-    public abstract <V> JSPromise<V> catchError(JSFunction<Object, V> onRejected);
+    public native <V> JSPromise<V> catchError(JSFunction<Object, V> onRejected);
 
     /** Call {@code onRejected} with the reject reason, returning a new promise. */
     @JSMethod("catch")
-    public abstract <V> JSPromise<V> flatCatchError(JSFunction<Object, ? extends JSPromise<V>> onRejected);
+    public native <V> JSPromise<V> flatCatchError(JSFunction<Object, ? extends JSPromise<V>> onRejected);
 
     /** Call {@code onFinally} after settling, ignoring the return value. */
     @JSMethod("finally")
-    public abstract JSPromise<T> onSettled(JSSupplier<Object> onFinally);
+    public native JSPromise<T> onSettled(JSSupplier<Object> onFinally);
 
     /** Interface for the return values of {@ref #allSettled()}. */
     public interface FulfillmentValue<T> extends JSObject {
diff --git a/jso/apis/src/main/java/org/teavm/jso/core/JSRegExp.java b/jso/apis/src/main/java/org/teavm/jso/core/JSRegExp.java
index 9f34c8552..69ff32622 100644
--- a/jso/apis/src/main/java/org/teavm/jso/core/JSRegExp.java
+++ b/jso/apis/src/main/java/org/teavm/jso/core/JSRegExp.java
@@ -17,16 +17,26 @@ package org.teavm.jso.core;
 
 import org.teavm.interop.NoSideEffects;
 import org.teavm.jso.JSBody;
+import org.teavm.jso.JSClass;
 import org.teavm.jso.JSObject;
 import org.teavm.jso.JSProperty;
 
-public abstract class JSRegExp implements JSObject {
+@JSClass(name = "RegExp")
+public class JSRegExp implements JSObject {
+    public JSRegExp(String pattern) {
+    }
+
+    public JSRegExp(String pattern, String flags) {
+    }
+
     @JSBody(params = "pattern", script = "return new RegExp(pattern);")
     @NoSideEffects
+    @Deprecated
     public static native JSRegExp create(String pattern);
 
     @JSBody(params = { "pattern", "flags" }, script = "return new RegExp(pattern, flags);")
     @NoSideEffects
+    @Deprecated
     public static native JSRegExp create(String pattern, String flags);
 
     public static JSRegExp create(String pattern, JSRegExpFlag... flags) {
@@ -60,23 +70,23 @@ public abstract class JSRegExp implements JSObject {
     }
 
     @JSProperty
-    public abstract boolean isGlobal();
+    public native boolean isGlobal();
 
     @JSProperty
-    public abstract boolean isIgnoreCase();
+    public native boolean isIgnoreCase();
 
     @JSProperty
-    public abstract boolean isMultiline();
+    public native boolean isMultiline();
 
     @JSProperty
-    public abstract int getLastIndex();
+    public native int getLastIndex();
 
     @JSProperty
-    public abstract JSString getSource();
+    public native JSString getSource();
 
-    public abstract JSArray<JSString> exec(JSString text);
+    public native JSArray<JSString> exec(JSString text);
 
-    public abstract boolean test(JSString text);
+    public native boolean test(JSString text);
 
-    public abstract boolean test(String text);
+    public native boolean test(String text);
 }
diff --git a/jso/apis/src/main/java/org/teavm/jso/core/JSString.java b/jso/apis/src/main/java/org/teavm/jso/core/JSString.java
index 837e498ab..301746ef8 100644
--- a/jso/apis/src/main/java/org/teavm/jso/core/JSString.java
+++ b/jso/apis/src/main/java/org/teavm/jso/core/JSString.java
@@ -36,7 +36,6 @@ public abstract class JSString implements JSObject {
     @NoSideEffects
     public static native JSString valueOf(String str);
 
-    @JSBody(params = "code", script = "return String.fromCharCode(code)")
     @NoSideEffects
     public static native JSString fromCharCode(int code);
 
diff --git a/jso/apis/src/main/java/org/teavm/jso/core/JSSymbol.java b/jso/apis/src/main/java/org/teavm/jso/core/JSSymbol.java
index d528693e2..ce34f8fb5 100644
--- a/jso/apis/src/main/java/org/teavm/jso/core/JSSymbol.java
+++ b/jso/apis/src/main/java/org/teavm/jso/core/JSSymbol.java
@@ -19,7 +19,10 @@ import org.teavm.interop.NoSideEffects;
 import org.teavm.jso.JSBody;
 import org.teavm.jso.JSObject;
 
-public abstract class JSSymbol<T> implements JSObject {
+public class JSSymbol<T> implements JSObject {
+    private JSSymbol() {
+    }
+
     @JSBody(params = "name", script = "return Symbol(name);")
     public static native <T> JSSymbol<T> create(String name);
 
diff --git a/jso/apis/src/main/java/org/teavm/jso/core/JSWeakMap.java b/jso/apis/src/main/java/org/teavm/jso/core/JSWeakMap.java
index 196ffe72b..f7498a0a3 100644
--- a/jso/apis/src/main/java/org/teavm/jso/core/JSWeakMap.java
+++ b/jso/apis/src/main/java/org/teavm/jso/core/JSWeakMap.java
@@ -17,19 +17,25 @@ package org.teavm.jso.core;
 
 import org.teavm.interop.NoSideEffects;
 import org.teavm.jso.JSBody;
+import org.teavm.jso.JSClass;
 import org.teavm.jso.JSObject;
 
-public abstract class JSWeakMap<K, V> implements JSObject {
-    public abstract V get(K key);
+@JSClass(name = "WeakMap")
+public class JSWeakMap<K, V> implements JSObject {
+    public JSWeakMap() {
+    }
 
-    public abstract boolean has(K key);
+    public native V get(K key);
 
-    public abstract JSWeakMap<K, V> set(K key, V value);
+    public native boolean has(K key);
 
-    public abstract boolean remove(K key);
+    public native JSWeakMap<K, V> set(K key, V value);
+
+    public native boolean remove(K key);
 
     @JSBody(script = "return new WeakMap();")
     @NoSideEffects
+    @Deprecated
     public static native <K, V> JSWeakMap<K, V> create();
 
     @JSBody(script = "return typeof WeakMap !== 'undefined';")
diff --git a/jso/apis/src/main/java/org/teavm/jso/core/JSWeakRef.java b/jso/apis/src/main/java/org/teavm/jso/core/JSWeakRef.java
index 98680ff04..47ab05d3a 100644
--- a/jso/apis/src/main/java/org/teavm/jso/core/JSWeakRef.java
+++ b/jso/apis/src/main/java/org/teavm/jso/core/JSWeakRef.java
@@ -17,13 +17,19 @@ package org.teavm.jso.core;
 
 import org.teavm.interop.NoSideEffects;
 import org.teavm.jso.JSBody;
+import org.teavm.jso.JSClass;
 import org.teavm.jso.JSObject;
 
+@JSClass(name = "WeakRef")
 public abstract class JSWeakRef<T> implements JSObject {
+    public JSWeakRef(T value) {
+    }
+
     public abstract T deref();
 
     @JSBody(params = "value", script = "return new WeakRef(value);")
     @NoSideEffects
+    @Deprecated
     public static native <T> JSWeakRef<T> create(T value);
 
     @JSBody(script = "return typeof WeakRef !== 'undefined';")
diff --git a/jso/apis/src/main/java/org/teavm/jso/json/JSON.java b/jso/apis/src/main/java/org/teavm/jso/json/JSON.java
index ff9073b1d..187c4f3e5 100644
--- a/jso/apis/src/main/java/org/teavm/jso/json/JSON.java
+++ b/jso/apis/src/main/java/org/teavm/jso/json/JSON.java
@@ -15,16 +15,15 @@
  */
 package org.teavm.jso.json;
 
-import org.teavm.jso.JSBody;
+import org.teavm.jso.JSClass;
 import org.teavm.jso.JSObject;
 
-public final class JSON {
+@JSClass
+public final class JSON implements JSObject {
     private JSON() {
     }
 
-    @JSBody(params = "object", script = "return JSON.stringify(object);")
     public static native String stringify(JSObject object);
 
-    @JSBody(params = "string", script = "return JSON.parse(string);")
     public static native JSObject parse(String string);
 }
diff --git a/jso/apis/src/main/java/org/teavm/jso/typedarrays/ArrayBuffer.java b/jso/apis/src/main/java/org/teavm/jso/typedarrays/ArrayBuffer.java
index 5d4b79c18..28cb3c628 100644
--- a/jso/apis/src/main/java/org/teavm/jso/typedarrays/ArrayBuffer.java
+++ b/jso/apis/src/main/java/org/teavm/jso/typedarrays/ArrayBuffer.java
@@ -16,15 +16,24 @@
 package org.teavm.jso.typedarrays;
 
 import org.teavm.jso.JSBody;
+import org.teavm.jso.JSClass;
 import org.teavm.jso.JSObject;
 import org.teavm.jso.JSProperty;
 
-public abstract class ArrayBuffer implements JSObject {
-    @JSProperty
-    public abstract int getByteLength();
+@JSClass
+public class ArrayBuffer implements JSObject {
+    public ArrayBuffer() {
+    }
 
-    public abstract ArrayBuffer slice(int begin, int end);
+    public ArrayBuffer(int length) {
+    }
+
+    @JSProperty
+    public native int getByteLength();
+
+    public native ArrayBuffer slice(int begin, int end);
 
     @JSBody(params = "length", script = "return new ArrayBuffer(length);")
+    @Deprecated
     public static native ArrayBuffer create(int length);
 }
diff --git a/jso/apis/src/main/java/org/teavm/jso/typedarrays/ArrayBufferView.java b/jso/apis/src/main/java/org/teavm/jso/typedarrays/ArrayBufferView.java
index 76921ec5c..0dda80145 100644
--- a/jso/apis/src/main/java/org/teavm/jso/typedarrays/ArrayBufferView.java
+++ b/jso/apis/src/main/java/org/teavm/jso/typedarrays/ArrayBufferView.java
@@ -25,42 +25,42 @@ public abstract class ArrayBufferView implements JSObject {
     }
 
     @JSProperty
-    public abstract int getLength();
+    public native int getLength();
 
     @JSProperty
-    public abstract int getByteLength();
+    public native int getByteLength();
 
     @JSProperty
-    public abstract int getByteOffset();
+    public native int getByteOffset();
 
     @JSProperty
-    public abstract ArrayBuffer getBuffer();
+    public native ArrayBuffer getBuffer();
 
-    public abstract void set(ArrayBufferView other, int offset);
+    public native void set(ArrayBufferView other, int offset);
 
-    public abstract void set(ArrayBufferView other);
+    public native void set(ArrayBufferView other);
 
-    public abstract void set(JSArrayReader<?> other, int offset);
+    public native void set(JSArrayReader<?> other, int offset);
 
-    public abstract void set(JSArrayReader<?> other);
+    public native void set(JSArrayReader<?> other);
 
-    public abstract void set(@JSByRef byte[] other, int offset);
+    public native void set(@JSByRef byte[] other, int offset);
 
-    public abstract void set(@JSByRef byte[] other);
+    public native void set(@JSByRef byte[] other);
 
-    public abstract void set(@JSByRef short[] other, int offset);
+    public native void set(@JSByRef short[] other, int offset);
 
-    public abstract void set(@JSByRef short[] other);
+    public native void set(@JSByRef short[] other);
 
-    public abstract void set(@JSByRef int[] other, int offset);
+    public native void set(@JSByRef int[] other, int offset);
 
-    public abstract void set(@JSByRef int[] other);
+    public native void set(@JSByRef int[] other);
 
-    public abstract void set(@JSByRef float[] other, int offset);
+    public native void set(@JSByRef float[] other, int offset);
 
-    public abstract void set(@JSByRef float[] other);
+    public native void set(@JSByRef float[] other);
 
-    public abstract void set(@JSByRef double[] other, int offset);
+    public native void set(@JSByRef double[] other, int offset);
 
-    public abstract void set(@JSByRef double[] other);
+    public native void set(@JSByRef double[] other);
 }
diff --git a/jso/apis/src/main/java/org/teavm/jso/typedarrays/DataView.java b/jso/apis/src/main/java/org/teavm/jso/typedarrays/DataView.java
index dd851a37f..19ba22536 100644
--- a/jso/apis/src/main/java/org/teavm/jso/typedarrays/DataView.java
+++ b/jso/apis/src/main/java/org/teavm/jso/typedarrays/DataView.java
@@ -16,65 +16,80 @@
 package org.teavm.jso.typedarrays;
 
 import org.teavm.jso.JSBody;
+import org.teavm.jso.JSClass;
 
-public abstract class DataView extends ArrayBufferView {
-    public abstract byte getInt8(int byteOffset);
+@JSClass
+public class DataView extends ArrayBufferView {
+    public DataView(ArrayBuffer buffer) {
+    }
 
-    public abstract short getUint8(int byteOffset);
+    public DataView(ArrayBufferView buffer) {
+    }
 
-    public abstract short getInt16(int byteOffset);
+    public DataView(ArrayBuffer buffer, int offset, int length) {
+    }
 
-    public abstract short getInt16(int byteOffset, boolean littleEndian);
+    public DataView(ArrayBuffer buffer, int offset) {
+    }
 
-    public abstract int getUint16(int byteOffset);
+    public native byte getInt8(int byteOffset);
 
-    public abstract int getUint16(int byteOffset, boolean littleEndian);
+    public native short getUint8(int byteOffset);
 
-    public abstract int getInt32(int byteOffset);
+    public native short getInt16(int byteOffset);
 
-    public abstract int getInt32(int byteOffset, boolean littleEndian);
+    public native short getInt16(int byteOffset, boolean littleEndian);
 
-    public abstract int getUint32(int byteOffset);
+    public native int getUint16(int byteOffset);
 
-    public abstract int getUint32(int byteOffset, boolean littleEndian);
+    public native int getUint16(int byteOffset, boolean littleEndian);
 
-    public abstract float getFloat32(int byteOffset);
+    public native int getInt32(int byteOffset);
 
-    public abstract float getFloat32(int byteOffset, boolean littleEndian);
+    public native int getInt32(int byteOffset, boolean littleEndian);
 
-    public abstract double getFloat64(int byteOffset);
+    public native int getUint32(int byteOffset);
 
-    public abstract double getFloat64(int byteOffset, boolean littleEndian);
+    public native int getUint32(int byteOffset, boolean littleEndian);
 
-    public abstract void setInt8(int byteOffset, int value);
+    public native float getFloat32(int byteOffset);
 
-    public abstract void setUint8(int byteOffset, int value);
+    public native float getFloat32(int byteOffset, boolean littleEndian);
 
-    public abstract void setInt16(int byteOffset, int value);
+    public native double getFloat64(int byteOffset);
 
-    public abstract void setInt16(int byteOffset, int value, boolean littleEndian);
+    public native double getFloat64(int byteOffset, boolean littleEndian);
 
-    public abstract void setUint16(int byteOffset, int value);
+    public native void setInt8(int byteOffset, int value);
 
-    public abstract void setUint16(int byteOffset, int value, boolean littleEndian);
+    public native void setUint8(int byteOffset, int value);
 
-    public abstract void setInt32(int byteOffset, int value);
+    public native void setInt16(int byteOffset, int value);
 
-    public abstract void setInt32(int byteOffset, int value, boolean littleEndian);
+    public native void setInt16(int byteOffset, int value, boolean littleEndian);
 
-    public abstract void setUint32(int byteOffset, int value);
+    public native void setUint16(int byteOffset, int value);
 
-    public abstract void setUint32(int byteOffset, int value, boolean littleEndian);
+    public native void setUint16(int byteOffset, int value, boolean littleEndian);
 
-    public abstract void setFloat32(int byteOffset, float value);
+    public native void setInt32(int byteOffset, int value);
 
-    public abstract void setFloat32(int byteOffset, float value, boolean littleEndian);
+    public native void setInt32(int byteOffset, int value, boolean littleEndian);
 
-    public abstract void setFloat64(int byteOffset, double value);
+    public native void setUint32(int byteOffset, int value);
 
-    public abstract void setFloat64(int byteOffset, double value, boolean littleEndian);
+    public native void setUint32(int byteOffset, int value, boolean littleEndian);
+
+    public native void setFloat32(int byteOffset, float value);
+
+    public native void setFloat32(int byteOffset, float value, boolean littleEndian);
+
+    public native void setFloat64(int byteOffset, double value);
+
+    public native void setFloat64(int byteOffset, double value, boolean littleEndian);
 
     @JSBody(params = "buffer", script = "return new DataView(buffer);")
+    @Deprecated
     public static native DataView create(ArrayBuffer buffer);
 
     @JSBody(params = "buffer", script = "return new DataView(buffer);")
diff --git a/jso/apis/src/main/java/org/teavm/jso/typedarrays/Float32Array.java b/jso/apis/src/main/java/org/teavm/jso/typedarrays/Float32Array.java
index 64647df3a..1ddc4ee06 100644
--- a/jso/apis/src/main/java/org/teavm/jso/typedarrays/Float32Array.java
+++ b/jso/apis/src/main/java/org/teavm/jso/typedarrays/Float32Array.java
@@ -16,27 +16,49 @@
 package org.teavm.jso.typedarrays;
 
 import org.teavm.jso.JSBody;
+import org.teavm.jso.JSClass;
 import org.teavm.jso.JSIndexer;
 
-public abstract class Float32Array extends ArrayBufferView {
-    @JSIndexer
-    public abstract float get(int index);
+@JSClass
+public class Float32Array extends ArrayBufferView {
+    public Float32Array(int length) {
+    }
+
+    public Float32Array(ArrayBuffer buffer) {
+    }
+
+    public Float32Array(ArrayBufferView buffer) {
+    }
+
+    public Float32Array(ArrayBuffer buffer, int offset, int length) {
+    }
+
+    public Float32Array(ArrayBuffer buffer, int offset) {
+    }
 
     @JSIndexer
-    public abstract void set(int index, float value);
+    public native float get(int index);
+
+    @JSIndexer
+    public native void set(int index, float value);
 
     @JSBody(params = "length", script = "return new Float32Array(length);")
+    @Deprecated
     public static native Float32Array create(int length);
 
     @JSBody(params = "buffer", script = "return new Float32Array(buffer);")
+    @Deprecated
     public static native Float32Array create(ArrayBuffer buffer);
 
     @JSBody(params = "buffer", script = "return new Float32Array(buffer);")
+    @Deprecated
     public static native Float32Array create(ArrayBufferView buffer);
 
     @JSBody(params = { "buffer", "offset", "length" }, script = "return new Float32Array(buffer, offset, length);")
+    @Deprecated
     public static native Float32Array create(ArrayBuffer buffer, int offset, int length);
 
     @JSBody(params = { "buffer", "offset" }, script = "return new Float32Array(buffer, offset);")
+    @Deprecated
     public static native Float32Array create(ArrayBuffer buffer, int offset);
 }
diff --git a/jso/apis/src/main/java/org/teavm/jso/typedarrays/Float64Array.java b/jso/apis/src/main/java/org/teavm/jso/typedarrays/Float64Array.java
index d1379f931..815336a43 100644
--- a/jso/apis/src/main/java/org/teavm/jso/typedarrays/Float64Array.java
+++ b/jso/apis/src/main/java/org/teavm/jso/typedarrays/Float64Array.java
@@ -16,27 +16,49 @@
 package org.teavm.jso.typedarrays;
 
 import org.teavm.jso.JSBody;
+import org.teavm.jso.JSClass;
 import org.teavm.jso.JSIndexer;
 
-public abstract class Float64Array extends ArrayBufferView {
-    @JSIndexer
-    public abstract double get(int index);
+@JSClass
+public class Float64Array extends ArrayBufferView {
+    public Float64Array(int length) {
+    }
+
+    public Float64Array(ArrayBuffer buffer) {
+    }
+
+    public Float64Array(ArrayBufferView buffer) {
+    }
+
+    public Float64Array(ArrayBuffer buffer, int offset, int length) {
+    }
+
+    public Float64Array(ArrayBuffer buffer, int offset) {
+    }
 
     @JSIndexer
-    public abstract void set(int index, double value);
+    public native double get(int index);
+
+    @JSIndexer
+    public native void set(int index, double value);
 
     @JSBody(params = "length", script = "return new Float64Array(length);")
+    @Deprecated
     public static native Float64Array create(int length);
 
     @JSBody(params = "buffer", script = "return new Float64Array(buffer);")
+    @Deprecated
     public static native Float64Array create(ArrayBuffer buffer);
 
     @JSBody(params = "buffer", script = "return new Float64Array(buffer);")
+    @Deprecated
     public static native Float64Array create(ArrayBufferView buffer);
 
     @JSBody(params = { "buffer", "offset", "length" }, script = "return new Float64Array(buffer, offset, length);")
+    @Deprecated
     public static native Float64Array create(ArrayBuffer buffer, int offset, int length);
 
     @JSBody(params = { "buffer", "offset" }, script = "return new Float64Array(buffer, offset);")
+    @Deprecated
     public static native Float64Array create(ArrayBuffer buffer, int offset);
 }
diff --git a/jso/apis/src/main/java/org/teavm/jso/typedarrays/Int16Array.java b/jso/apis/src/main/java/org/teavm/jso/typedarrays/Int16Array.java
index caaa5d0bf..87ce6444e 100644
--- a/jso/apis/src/main/java/org/teavm/jso/typedarrays/Int16Array.java
+++ b/jso/apis/src/main/java/org/teavm/jso/typedarrays/Int16Array.java
@@ -16,27 +16,49 @@
 package org.teavm.jso.typedarrays;
 
 import org.teavm.jso.JSBody;
+import org.teavm.jso.JSClass;
 import org.teavm.jso.JSIndexer;
 
-public abstract class Int16Array extends ArrayBufferView {
-    @JSIndexer
-    public abstract short get(int index);
+@JSClass
+public class Int16Array extends ArrayBufferView {
+    public Int16Array(int length) {
+    }
+
+    public Int16Array(ArrayBuffer buffer) {
+    }
+
+    public Int16Array(ArrayBufferView buffer) {
+    }
+
+    public Int16Array(ArrayBuffer buffer, int offset, int length) {
+    }
+
+    public Int16Array(ArrayBuffer buffer, int offset) {
+    }
 
     @JSIndexer
-    public abstract void set(int index, short value);
+    public native short get(int index);
+
+    @JSIndexer
+    public native void set(int index, short value);
 
     @JSBody(params = "length", script = "return new Int16Array(length);")
+    @Deprecated
     public static native Int16Array create(int length);
 
     @JSBody(params = "buffer", script = "return new Int16Array(buffer);")
+    @Deprecated
     public static native Int16Array create(ArrayBuffer buffer);
 
     @JSBody(params = "buffer", script = "return new Int16Array(buffer);")
+    @Deprecated
     public static native Int16Array create(ArrayBufferView buffer);
 
     @JSBody(params = { "buffer", "offset", "length" }, script = "return new Int16Array(buffer, offset, length);")
+    @Deprecated
     public static native Int16Array create(ArrayBuffer buffer, int offset, int length);
 
     @JSBody(params = { "buffer", "offset" }, script = "return new Int16Array(buffer, offset);")
+    @Deprecated
     public static native Int16Array create(ArrayBuffer buffer, int offset);
 }
diff --git a/jso/apis/src/main/java/org/teavm/jso/typedarrays/Int32Array.java b/jso/apis/src/main/java/org/teavm/jso/typedarrays/Int32Array.java
index 0f92358c9..9dbaf3e81 100644
--- a/jso/apis/src/main/java/org/teavm/jso/typedarrays/Int32Array.java
+++ b/jso/apis/src/main/java/org/teavm/jso/typedarrays/Int32Array.java
@@ -17,31 +17,55 @@ package org.teavm.jso.typedarrays;
 
 import org.teavm.jso.JSBody;
 import org.teavm.jso.JSByRef;
+import org.teavm.jso.JSClass;
 import org.teavm.jso.JSIndexer;
 
-public abstract class Int32Array extends ArrayBufferView {
-    @JSIndexer
-    public abstract int get(int index);
+@JSClass
+public class Int32Array extends ArrayBufferView {
+    public Int32Array(int length) {
+    }
+
+    public Int32Array(ArrayBuffer buffer) {
+    }
+
+    public Int32Array(ArrayBufferView buffer) {
+    }
+
+    public Int32Array(ArrayBuffer buffer, int offset, int length) {
+    }
+
+    public Int32Array(ArrayBuffer buffer, int offset) {
+    }
 
     @JSIndexer
-    public abstract void set(int index, int value);
+    public native int get(int index);
 
-    public abstract void set(@JSByRef int[] data, int offset);
+    @JSIndexer
+    public native void set(int index, int value);
 
-    public abstract void set(@JSByRef int[] data);
+    @Override
+    public native void set(@JSByRef int[] data, int offset);
+
+    @Override
+    public native void set(@JSByRef int[] data);
 
     @JSBody(params = "length", script = "return new Int32Array(length);")
+    @Deprecated
     public static native Int32Array create(int length);
 
     @JSBody(params = "buffer", script = "return new Int32Array(buffer);")
+    @Deprecated
     public static native Int32Array create(ArrayBuffer buffer);
 
     @JSBody(params = "buffer", script = "return new Int32Array(buffer);")
+    @Deprecated
     public static native Int32Array create(ArrayBufferView buffer);
 
     @JSBody(params = { "buffer", "offset", "length" }, script = "return new Int32Array(buffer, offset, length);")
+    @Deprecated
     public static native Int32Array create(ArrayBuffer buffer, int offset, int length);
 
     @JSBody(params = { "buffer", "offset" }, script = "return new Int32Array(buffer, offset);")
+    @Deprecated
     public static native Int32Array create(ArrayBuffer buffer, int offset);
 }
diff --git a/jso/apis/src/main/java/org/teavm/jso/typedarrays/Int8Array.java b/jso/apis/src/main/java/org/teavm/jso/typedarrays/Int8Array.java
index ed42d640c..c15c31546 100644
--- a/jso/apis/src/main/java/org/teavm/jso/typedarrays/Int8Array.java
+++ b/jso/apis/src/main/java/org/teavm/jso/typedarrays/Int8Array.java
@@ -16,27 +16,49 @@
 package org.teavm.jso.typedarrays;
 
 import org.teavm.jso.JSBody;
+import org.teavm.jso.JSClass;
 import org.teavm.jso.JSIndexer;
 
-public abstract class Int8Array extends ArrayBufferView {
-    @JSIndexer
-    public abstract byte get(int index);
+@JSClass
+public class Int8Array extends ArrayBufferView {
+    public Int8Array(int length) {
+    }
+
+    public Int8Array(ArrayBuffer buffer) {
+    }
+
+    public Int8Array(ArrayBufferView buffer) {
+    }
+
+    public Int8Array(ArrayBuffer buffer, int offset, int length) {
+    }
+
+    public Int8Array(ArrayBuffer buffer, int offset) {
+    }
 
     @JSIndexer
-    public abstract void set(int index, byte value);
+    public native byte get(int index);
+
+    @JSIndexer
+    public native void set(int index, byte value);
 
     @JSBody(params = "length", script = "return new Int8Array(length);")
+    @Deprecated
     public static native Int8Array create(int length);
 
     @JSBody(params = "buffer", script = "return new Int8Array(buffer);")
+    @Deprecated
     public static native Int8Array create(ArrayBuffer buffer);
 
     @JSBody(params = "buffer", script = "return new Int8Array(buffer);")
+    @Deprecated
     public static native Int8Array create(ArrayBufferView buffer);
 
     @JSBody(params = { "buffer", "offset", "length" }, script = "return new Int8Array(buffer, offset, length);")
+    @Deprecated
     public static native Int8Array create(ArrayBuffer buffer, int offset, int length);
 
     @JSBody(params = { "buffer", "offset" }, script = "return new Int8Array(buffer, offset);")
+    @Deprecated
     public static native Int8Array create(ArrayBuffer buffer, int offset);
 }
diff --git a/jso/apis/src/main/java/org/teavm/jso/typedarrays/Uint16Array.java b/jso/apis/src/main/java/org/teavm/jso/typedarrays/Uint16Array.java
index 177975a0f..fc130d2f1 100644
--- a/jso/apis/src/main/java/org/teavm/jso/typedarrays/Uint16Array.java
+++ b/jso/apis/src/main/java/org/teavm/jso/typedarrays/Uint16Array.java
@@ -16,27 +16,49 @@
 package org.teavm.jso.typedarrays;
 
 import org.teavm.jso.JSBody;
+import org.teavm.jso.JSClass;
 import org.teavm.jso.JSIndexer;
 
-public abstract class Uint16Array extends ArrayBufferView {
-    @JSIndexer
-    public abstract int get(int index);
+@JSClass
+public class Uint16Array extends ArrayBufferView {
+    public Uint16Array(int length) {
+    }
+
+    public Uint16Array(ArrayBuffer buffer) {
+    }
+
+    public Uint16Array(ArrayBufferView buffer) {
+    }
+
+    public Uint16Array(ArrayBuffer buffer, int offset, int length) {
+    }
+
+    public Uint16Array(ArrayBuffer buffer, int offset) {
+    }
 
     @JSIndexer
-    public abstract void set(int index, int value);
+    public native int get(int index);
+
+    @JSIndexer
+    public native void set(int index, int value);
 
     @JSBody(params = "length", script = "return new Uint16Array(length);")
+    @Deprecated
     public static native Uint16Array create(int length);
 
     @JSBody(params = "buffer", script = "return new Uint16Array(buffer);")
+    @Deprecated
     public static native Uint16Array create(ArrayBuffer buffer);
 
     @JSBody(params = "buffer", script = "return new Uint16Array(buffer);")
+    @Deprecated
     public static native Uint16Array create(ArrayBufferView buffer);
 
     @JSBody(params = { "buffer", "offset", "length" }, script = "return new Uint16Array(buffer, offset, length);")
+    @Deprecated
     public static native Uint16Array create(ArrayBuffer buffer, int offset, int length);
 
     @JSBody(params = { "buffer", "offset" }, script = "return new Uint16Array(buffer, offset);")
+    @Deprecated
     public static native Uint16Array create(ArrayBuffer buffer, int offset);
 }
diff --git a/jso/apis/src/main/java/org/teavm/jso/typedarrays/Uint8Array.java b/jso/apis/src/main/java/org/teavm/jso/typedarrays/Uint8Array.java
index dbb1fc959..9a41b0c93 100644
--- a/jso/apis/src/main/java/org/teavm/jso/typedarrays/Uint8Array.java
+++ b/jso/apis/src/main/java/org/teavm/jso/typedarrays/Uint8Array.java
@@ -16,27 +16,49 @@
 package org.teavm.jso.typedarrays;
 
 import org.teavm.jso.JSBody;
+import org.teavm.jso.JSClass;
 import org.teavm.jso.JSIndexer;
 
-public abstract class Uint8Array extends ArrayBufferView {
-    @JSIndexer
-    public abstract short get(int index);
+@JSClass
+public class Uint8Array extends ArrayBufferView {
+    public Uint8Array(int length) {
+    }
+
+    public Uint8Array(ArrayBuffer buffer) {
+    }
+
+    public Uint8Array(ArrayBufferView buffer) {
+    }
+
+    public Uint8Array(ArrayBuffer buffer, int offset, int length) {
+    }
+
+    public Uint8Array(ArrayBuffer buffer, int offset) {
+    }
 
     @JSIndexer
-    public abstract void set(int index, short value);
+    public native short get(int index);
+
+    @JSIndexer
+    public native void set(int index, short value);
 
     @JSBody(params = "length", script = "return new Uint8Array(length);")
+    @Deprecated
     public static native Uint8Array create(int length);
 
     @JSBody(params = "buffer", script = "return new Uint8Array(buffer);")
+    @Deprecated
     public static native Uint8Array create(ArrayBuffer buffer);
 
     @JSBody(params = "buffer", script = "return new Uint8Array(buffer);")
+    @Deprecated
     public static native Uint8Array create(ArrayBufferView buffer);
 
     @JSBody(params = { "buffer", "offset", "length" }, script = "return new Uint8Array(buffer, offset, length);")
+    @Deprecated
     public static native Uint8Array create(ArrayBuffer buffer, int offset, int length);
 
     @JSBody(params = { "buffer", "offset" }, script = "return new Uint8Array(buffer, offset);")
+    @Deprecated
     public static native Uint8Array create(ArrayBuffer buffer, int offset);
 }
diff --git a/jso/apis/src/main/java/org/teavm/jso/typedarrays/Uint8ClampedArray.java b/jso/apis/src/main/java/org/teavm/jso/typedarrays/Uint8ClampedArray.java
index 6fbdb6545..6d45b7d2e 100644
--- a/jso/apis/src/main/java/org/teavm/jso/typedarrays/Uint8ClampedArray.java
+++ b/jso/apis/src/main/java/org/teavm/jso/typedarrays/Uint8ClampedArray.java
@@ -16,26 +16,47 @@
 package org.teavm.jso.typedarrays;
 
 import org.teavm.jso.JSBody;
+import org.teavm.jso.JSClass;
 import org.teavm.jso.JSIndexer;
 
-public abstract class Uint8ClampedArray extends ArrayBufferView {
-    @JSIndexer
-    public abstract short get(int index);
+@JSClass
+public class Uint8ClampedArray extends ArrayBufferView {
+    public Uint8ClampedArray(int length) {
+    }
+
+    public Uint8ClampedArray(ArrayBuffer buffer) {
+    }
+
+    public Uint8ClampedArray(ArrayBufferView buffer) {
+    }
+
+    public Uint8ClampedArray(ArrayBuffer buffer, int offset, int length) {
+    }
+
+    public Uint8ClampedArray(ArrayBuffer buffer, int offset) {
+    }
 
     @JSIndexer
-    public abstract void set(int index, int value);
+    public native short get(int index);
+
+    @JSIndexer
+    public native void set(int index, int value);
 
     @JSBody(params = "length", script = "return new Uint8ClampedArray(length);")
+    @Deprecated
     public static native Uint8ClampedArray create(int length);
 
     @JSBody(params = "buffer", script = "return new Uint8ClampedArray(buffer);")
+    @Deprecated
     public static native Uint8ClampedArray create(ArrayBuffer buffer);
 
     @JSBody(params = "buffer", script = "return new Uint8ClampedArray(buffer);")
+    @Deprecated
     public static native Uint8ClampedArray create(ArrayBufferView buffer);
 
     @JSBody(params = { "buffer", "offset", "length" }, script = "return new "
             + "Uint8ClampedArray(buffer, offset, length);")
+    @Deprecated
     public static native Uint8ClampedArray create(ArrayBuffer buffer, int offset, int length);
 
     @JSBody(params = { "buffer", "offset" }, script = "return new Uint8ClampedArray(buffer, offset);")
diff --git a/jso/apis/src/main/java/org/teavm/jso/webaudio/AudioContext.java b/jso/apis/src/main/java/org/teavm/jso/webaudio/AudioContext.java
index 50718756b..b98a8e58a 100644
--- a/jso/apis/src/main/java/org/teavm/jso/webaudio/AudioContext.java
+++ b/jso/apis/src/main/java/org/teavm/jso/webaudio/AudioContext.java
@@ -17,6 +17,7 @@ package org.teavm.jso.webaudio;
 
 import org.teavm.jso.JSBody;
 import org.teavm.jso.JSByRef;
+import org.teavm.jso.JSClass;
 import org.teavm.jso.JSObject;
 import org.teavm.jso.JSProperty;
 import org.teavm.jso.dom.events.EventListener;
@@ -24,108 +25,110 @@ import org.teavm.jso.dom.html.HTMLMediaElement;
 import org.teavm.jso.typedarrays.ArrayBuffer;
 import org.teavm.jso.typedarrays.Float32Array;
 
-public abstract class AudioContext implements JSObject {
+@JSClass
+public class AudioContext implements JSObject {
     public static final String STATE_SUSPENDED = "suspended";
     public static final String STATE_RUNNING = "running";
     public static final String STATE_CLOSE = "close";
 
     @JSProperty
-    public abstract AudioDestinationNode getDestination();
+    public native AudioDestinationNode getDestination();
 
     @JSProperty
-    public abstract float getSampleRate();
+    public native float getSampleRate();
 
     @JSProperty
-    public abstract double getCurrentTime();
+    public native double getCurrentTime();
 
     @JSProperty
-    public abstract AudioListener getListener();
+    public native AudioListener getListener();
 
     @JSProperty
-    public abstract String getState();
+    public native String getState();
 
     @JSProperty("onstatechange")
-    public abstract void setOnStateChange(EventListener<MediaEvent> listener);
+    public native void setOnStateChange(EventListener<MediaEvent> listener);
 
     @JSProperty("onstatechange")
-    public abstract EventListener<MediaEvent> getOnStateChange();
+    public native EventListener<MediaEvent> getOnStateChange();
 
-    public abstract void suspend();
+    public native void suspend();
 
-    public abstract void resume();
+    public native void resume();
 
-    public abstract void close();
+    public native void close();
 
-    public abstract AudioBuffer createBuffer(int numberOfChannels, int length, float sampleRate);
+    public native AudioBuffer createBuffer(int numberOfChannels, int length, float sampleRate);
 
-    public abstract AudioBuffer decodeAudioData(ArrayBuffer audioData, DecodeSuccessCallback successCallback,
+    public native AudioBuffer decodeAudioData(ArrayBuffer audioData, DecodeSuccessCallback successCallback,
             DecodeErrorCallback errorCallback);
 
-    public abstract AudioBuffer decodeAudioData(ArrayBuffer audioData, DecodeSuccessCallback successCallback);
+    public native AudioBuffer decodeAudioData(ArrayBuffer audioData, DecodeSuccessCallback successCallback);
 
-    public abstract AudioBuffer decodeAudioData(ArrayBuffer audioData);
+    public native AudioBuffer decodeAudioData(ArrayBuffer audioData);
 
-    public abstract AudioBufferSourceNode createBufferSource();
+    public native AudioBufferSourceNode createBufferSource();
 
-    public abstract MediaElementAudioSourceNode createMediaElementSource(HTMLMediaElement mediaElement);
+    public native MediaElementAudioSourceNode createMediaElementSource(HTMLMediaElement mediaElement);
 
-    public abstract MediaStreamAudioSourceNode createMediaStreamSource(MediaStream mediaStream);
+    public native MediaStreamAudioSourceNode createMediaStreamSource(MediaStream mediaStream);
 
-    public abstract MediaStreamAudioDestinationNode createMediaStreamDestination();
+    public native MediaStreamAudioDestinationNode createMediaStreamDestination();
 
-    public abstract AudioWorker createAudioWorker();
+    public native AudioWorker createAudioWorker();
 
-    public abstract ScriptProcessorNode createScriptProcessor(int bufferSize, int numberOfInputChannels,
+    public native ScriptProcessorNode createScriptProcessor(int bufferSize, int numberOfInputChannels,
             int numberOfOutputChannels);
 
-    public abstract ScriptProcessorNode createScriptProcessor(int bufferSize, int numberOfInputChannels);
+    public native ScriptProcessorNode createScriptProcessor(int bufferSize, int numberOfInputChannels);
 
-    public abstract ScriptProcessorNode createScriptProcessor(int bufferSize);
+    public native ScriptProcessorNode createScriptProcessor(int bufferSize);
 
-    public abstract ScriptProcessorNode createScriptProcessor();
+    public native ScriptProcessorNode createScriptProcessor();
 
-    public abstract AnalyserNode createAnalyser();
+    public native AnalyserNode createAnalyser();
 
-    public abstract GainNode createGain();
+    public native GainNode createGain();
 
-    public abstract DelayNode createDelay(double maxDelayTime);
+    public native DelayNode createDelay(double maxDelayTime);
 
-    public abstract DelayNode createDelay();
+    public native DelayNode createDelay();
 
-    public abstract BiquadFilterNode createBiquadFilter();
+    public native BiquadFilterNode createBiquadFilter();
 
-    public abstract IIRFilterNode createIIRFilter(Float32Array feedforward, Float32Array feedback);
+    public native IIRFilterNode createIIRFilter(Float32Array feedforward, Float32Array feedback);
 
-    public abstract WaveShaperNode createWaveShaper();
+    public native WaveShaperNode createWaveShaper();
 
-    public abstract PannerNode createPanner();
+    public native PannerNode createPanner();
 
-    public abstract StereoPannerNode createStereoPanner();
+    public native StereoPannerNode createStereoPanner();
 
-    public abstract ConvolverNode createConvolver();
+    public native ConvolverNode createConvolver();
 
-    public abstract ChannelSplitterNode createChannelSplitter(int numberOfOutputs);
+    public native ChannelSplitterNode createChannelSplitter(int numberOfOutputs);
 
-    public abstract ChannelSplitterNode createChannelSplitter();
+    public native ChannelSplitterNode createChannelSplitter();
 
-    public abstract ChannelMergerNode createChannelMerger(int numberOfInputs);
+    public native ChannelMergerNode createChannelMerger(int numberOfInputs);
 
-    public abstract ChannelMergerNode createChannelMerger();
+    public native ChannelMergerNode createChannelMerger();
 
-    public abstract DynamicsCompressorNode createDynamicsCompressor();
+    public native DynamicsCompressorNode createDynamicsCompressor();
 
-    public abstract OscillatorNode createOscillator();
+    public native OscillatorNode createOscillator();
 
-    public abstract PeriodicWave createPeriodicWave(Float32Array real, Float32Array image,
+    public native PeriodicWave createPeriodicWave(Float32Array real, Float32Array image,
             PeriodicWaveConstraints constraints);
 
-    public abstract PeriodicWave createPeriodicWave(@JSByRef float[] real, @JSByRef float[] image,
+    public native PeriodicWave createPeriodicWave(@JSByRef float[] real, @JSByRef float[] image,
             PeriodicWaveConstraints constraints);
 
-    public abstract PeriodicWave createPeriodicWave(Float32Array real, Float32Array image);
+    public native PeriodicWave createPeriodicWave(Float32Array real, Float32Array image);
 
-    public abstract PeriodicWave createPeriodicWave(@JSByRef float[] real, @JSByRef float[] image);
+    public native PeriodicWave createPeriodicWave(@JSByRef float[] real, @JSByRef float[] image);
 
-    @JSBody(script = "var Context = window.AudioContext || window.webkitAudioContext; return new Context();")
+    @JSBody(script = "return new Context();")
+    @Deprecated
     public static native AudioContext create();
 }
diff --git a/jso/apis/src/main/java/org/teavm/jso/webaudio/OfflineAudioContext.java b/jso/apis/src/main/java/org/teavm/jso/webaudio/OfflineAudioContext.java
index f81d634a2..b98a036d9 100644
--- a/jso/apis/src/main/java/org/teavm/jso/webaudio/OfflineAudioContext.java
+++ b/jso/apis/src/main/java/org/teavm/jso/webaudio/OfflineAudioContext.java
@@ -15,20 +15,22 @@
  */
 package org.teavm.jso.webaudio;
 
+import org.teavm.jso.JSClass;
 import org.teavm.jso.JSProperty;
 import org.teavm.jso.dom.events.EventListener;
 
-public abstract class OfflineAudioContext extends AudioContext {
+@JSClass
+public class OfflineAudioContext extends AudioContext {
     @JSProperty("oncomplete")
-    public abstract void setOnComplete(EventListener<OfflineAudioCompletionEvent> event);
+    public native void setOnComplete(EventListener<OfflineAudioCompletionEvent> event);
 
     @JSProperty("oncomplete")
-    public abstract EventListener<OfflineAudioCompletionEvent> getOnComplete();
+    public native EventListener<OfflineAudioCompletionEvent> getOnComplete();
 
-    public abstract AudioBuffer startRendering();
+    public native AudioBuffer startRendering();
 
     @Override
-    public abstract void resume();
+    public native void resume();
 
-    public abstract void suspend(double suspendTime);
+    public native void suspend(double suspendTime);
 }
diff --git a/jso/apis/src/main/java/org/teavm/jso/websocket/WebSocket.java b/jso/apis/src/main/java/org/teavm/jso/websocket/WebSocket.java
index b58b6c632..ffbb4c521 100644
--- a/jso/apis/src/main/java/org/teavm/jso/websocket/WebSocket.java
+++ b/jso/apis/src/main/java/org/teavm/jso/websocket/WebSocket.java
@@ -16,6 +16,7 @@
 package org.teavm.jso.websocket;
 
 import org.teavm.jso.JSBody;
+import org.teavm.jso.JSClass;
 import org.teavm.jso.JSObject;
 import org.teavm.jso.JSProperty;
 import org.teavm.jso.dom.events.Event;
@@ -24,60 +25,73 @@ import org.teavm.jso.dom.events.MessageEvent;
 import org.teavm.jso.typedarrays.ArrayBuffer;
 import org.teavm.jso.typedarrays.ArrayBufferView;
 
-public abstract class WebSocket implements JSObject {
+@JSClass
+public class WebSocket implements JSObject {
+  public WebSocket(String url) {
+  }
+
+  public WebSocket(String url, String protocols) {
+  }
+
+  public WebSocket(String url, String[] protocols) {
+  }
+
   @JSProperty("onclose")
-  public abstract void onClose(EventListener<CloseEvent> eventListener);
+  public native void onClose(EventListener<CloseEvent> eventListener);
 
   @JSProperty("onerror")
-  public abstract void onError(EventListener<Event> eventListener);
+  public native void onError(EventListener<Event> eventListener);
 
   @JSProperty("onmessage")
-  public abstract void onMessage(EventListener<MessageEvent> eventListener);
+  public native void onMessage(EventListener<MessageEvent> eventListener);
 
   @JSProperty("onopen")
-  public abstract void onOpen(EventListener<Event> eventListener);
+  public native void onOpen(EventListener<Event> eventListener);
 
   @JSBody(params = "url", script = "return new WebSocket(url);")
+  @Deprecated
   public static native WebSocket create(String url);
 
   @JSBody(params = { "url", "protocols" }, script = "return new WebSocket(url, protocols);")
+  @Deprecated
   public static native WebSocket create(String url, String protocols);
 
   @JSBody(params = { "url", "protocols" }, script = "return new WebSocket(url, protocols);")
+  @Deprecated
   public static native WebSocket create(String url, String[] protocols);
 
-  public abstract void close();
+  public native void close();
 
-  public abstract void close(int code);
+  public native void close(int code);
 
-  public abstract void close(int code, String reason);
+  public native void close(int code, String reason);
 
-  public abstract void send(String data);
+  public native void send(String data);
 
-  public abstract void send(ArrayBuffer data);
+  public native void send(ArrayBuffer data);
 
-  public abstract void send(ArrayBufferView data);
+  public native void send(ArrayBufferView data);
 
   @JSProperty
-  public abstract String getBinaryType();
+  public native String getBinaryType();
 
   @JSProperty
-  public abstract void setBinaryType(String binaryType);
+  public native void setBinaryType(String binaryType);
 
   @JSProperty
-  public abstract int getBufferedAmount();
+  public native int getBufferedAmount();
 
   @JSProperty
-  public abstract String getExtensions();
+  public native String getExtensions();
 
   @JSProperty
-  public abstract String getProtocol();
+  public native String getProtocol();
 
   @JSProperty
-  public abstract int getReadyState();
+  public native int getReadyState();
 
   @JSProperty
-  public abstract String getUrl();
+  public native String getUrl();
 
   @JSBody(script = "return typeof WebSocket !== 'undefined';")
   public static native boolean isSupported();
diff --git a/jso/apis/src/main/java/org/teavm/jso/workers/SharedWorker.java b/jso/apis/src/main/java/org/teavm/jso/workers/SharedWorker.java
index c8165d280..ccd63a433 100644
--- a/jso/apis/src/main/java/org/teavm/jso/workers/SharedWorker.java
+++ b/jso/apis/src/main/java/org/teavm/jso/workers/SharedWorker.java
@@ -17,11 +17,36 @@ package org.teavm.jso.workers;
 
 import org.teavm.jso.JSBody;
 import org.teavm.jso.JSProperty;
+import org.teavm.jso.dom.events.ErrorEvent;
+import org.teavm.jso.dom.events.Event;
+import org.teavm.jso.dom.events.EventListener;
+
+public class SharedWorker implements AbstractWorker {
+    public SharedWorker(String url) {
+    }
 
-public abstract class SharedWorker implements AbstractWorker {
     @JSBody(params = "url", script = "return new SharedWorker(url);")
+    @Deprecated
     public static native Worker create(String url);
 
     @JSProperty
-    public abstract MessagePort getPort();
+    public native MessagePort getPort();
+
+    @Override
+    public native void onError(EventListener<ErrorEvent> listener);
+
+    @Override
+    public native void addEventListener(String type, EventListener<?> listener, boolean useCapture);
+
+    @Override
+    public native void addEventListener(String type, EventListener<?> listener);
+
+    @Override
+    public native void removeEventListener(String type, EventListener<?> listener, boolean useCapture);
+
+    @Override
+    public native void removeEventListener(String type, EventListener<?> listener);
+
+    @Override
+    public native boolean dispatchEvent(Event evt);
 }
diff --git a/jso/apis/src/main/java/org/teavm/jso/workers/Worker.java b/jso/apis/src/main/java/org/teavm/jso/workers/Worker.java
index 95876f9a4..63beb5e4b 100644
--- a/jso/apis/src/main/java/org/teavm/jso/workers/Worker.java
+++ b/jso/apis/src/main/java/org/teavm/jso/workers/Worker.java
@@ -16,18 +16,44 @@
 package org.teavm.jso.workers;
 
 import org.teavm.jso.JSBody;
+import org.teavm.jso.JSClass;
 import org.teavm.jso.JSProperty;
+import org.teavm.jso.dom.events.ErrorEvent;
+import org.teavm.jso.dom.events.Event;
 import org.teavm.jso.dom.events.EventListener;
 import org.teavm.jso.dom.events.MessageEvent;
 
-public abstract class Worker implements AbstractWorker {
+@JSClass
+public class Worker implements AbstractWorker {
+    public Worker(String url) {
+    }
+
     @JSBody(params = "url", script = "return new Worker(url);")
+    @Deprecated
     public static native Worker create(String url);
 
     @JSProperty("onmessage")
-    public abstract void onMessage(EventListener<MessageEvent> listener);
+    public native void onMessage(EventListener<MessageEvent> listener);
 
-    public abstract void postMessage(Object message);
+    public native void postMessage(Object message);
 
-    public abstract void terminate();
+    public native void terminate();
+
+    @Override
+    public native void onError(EventListener<ErrorEvent> listener);
+
+    @Override
+    public native void addEventListener(String type, EventListener<?> listener, boolean useCapture);
+
+    @Override
+    public native void addEventListener(String type, EventListener<?> listener);
+
+    @Override
+    public native void removeEventListener(String type, EventListener<?> listener, boolean useCapture);
+
+    @Override
+    public native void removeEventListener(String type, EventListener<?> listener);
+
+    @Override
+    public native boolean dispatchEvent(Event evt);
 }
diff --git a/jso/core/src/main/java/org/teavm/jso/JSModule.java b/jso/core/src/main/java/org/teavm/jso/JSModule.java
new file mode 100644
index 000000000..6715bd4cd
--- /dev/null
+++ b/jso/core/src/main/java/org/teavm/jso/JSModule.java
@@ -0,0 +1,27 @@
+/*
+ *  Copyright 2024 Alexey Andreev.
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.teavm.jso;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+public @interface JSModule {
+    String value();
+}
diff --git a/jso/impl/src/main/java/org/teavm/jso/impl/JS.java b/jso/impl/src/main/java/org/teavm/jso/impl/JS.java
index 76b762acf..3a53d789e 100644
--- a/jso/impl/src/main/java/org/teavm/jso/impl/JS.java
+++ b/jso/impl/src/main/java/org/teavm/jso/impl/JS.java
@@ -140,7 +140,7 @@ final class JS {
         if (array == null) {
             return null;
         }
-        JSArray<T> result = JSArray.create(array.length);
+        var result = new JSArray<T>(array.length);
         for (int i = 0; i < array.length; ++i) {
             result.set(i, array[i]);
         }
@@ -155,7 +155,7 @@ final class JS {
         if (array == null) {
             return null;
         }
-        JSArray<T> result = JSArray.create(array.length);
+        var result = new JSArray<T>(array.length);
         for (int i = 0; i < array.length; ++i) {
             result.set(i, f.apply(array[i]));
         }
@@ -178,7 +178,7 @@ final class JS {
         if (array == null) {
             return null;
         }
-        JSArray<JSBoolean> result = JSArray.create(array.length);
+        var result = new JSArray<JSBoolean>(array.length);
         for (int i = 0; i < array.length; ++i) {
             result.set(i, JSBoolean.valueOf(array[i]));
         }
@@ -193,7 +193,7 @@ final class JS {
         if (array == null) {
             return null;
         }
-        JSArray<JSNumber> result = JSArray.create(array.length);
+        var result = new JSArray<JSNumber>(array.length);
         for (int i = 0; i < array.length; ++i) {
             result.set(i, JSNumber.valueOf(array[i]));
         }
@@ -208,7 +208,7 @@ final class JS {
         if (array == null) {
             return null;
         }
-        JSArray<JSNumber> result = JSArray.create(array.length);
+        var result = new JSArray<JSNumber>(array.length);
         for (int i = 0; i < array.length; ++i) {
             result.set(i, JSNumber.valueOf(array[i]));
         }
@@ -223,7 +223,7 @@ final class JS {
         if (array == null) {
             return null;
         }
-        JSArray<JSNumber> result = JSArray.create(array.length);
+        var result = new JSArray<JSNumber>(array.length);
         for (int i = 0; i < array.length; ++i) {
             result.set(i, JSNumber.valueOf(array[i]));
         }
@@ -238,7 +238,7 @@ final class JS {
         if (array == null) {
             return null;
         }
-        JSArray<JSNumber> result = JSArray.create(array.length);
+        var result = new JSArray<JSNumber>(array.length);
         for (int i = 0; i < array.length; ++i) {
             result.set(i, JSNumber.valueOf(array[i]));
         }
@@ -253,7 +253,7 @@ final class JS {
         if (array == null) {
             return null;
         }
-        JSArray<JSString> result = JSArray.create(array.length);
+        var result = new JSArray<JSString>(array.length);
         for (int i = 0; i < array.length; ++i) {
             result.set(i, JSString.valueOf(array[i]));
         }
@@ -268,7 +268,7 @@ final class JS {
         if (array == null) {
             return null;
         }
-        JSArray<JSNumber> result = JSArray.create(array.length);
+        var result = new JSArray<JSNumber>(array.length);
         for (int i = 0; i < array.length; ++i) {
             result.set(i, JSNumber.valueOf(array[i]));
         }
@@ -283,7 +283,7 @@ final class JS {
         if (array == null) {
             return null;
         }
-        JSArray<JSNumber> result = JSArray.create(array.length);
+        var result = new JSArray<JSNumber>(array.length);
         for (int i = 0; i < array.length; ++i) {
             result.set(i, JSNumber.valueOf(array[i]));
         }
@@ -516,6 +516,71 @@ final class JS {
             JSObject d, JSObject e, JSObject f, JSObject g, JSObject h, JSObject i, JSObject j, JSObject k,
             JSObject l, JSObject m);
 
+    @InjectedBy(JSNativeInjector.class)
+    @PluggableDependency(JSNativeInjector.class)
+    public static native JSObject construct(JSObject cls);
+
+    @InjectedBy(JSNativeInjector.class)
+    @PluggableDependency(JSNativeInjector.class)
+    public static native JSObject construct(JSObject cls, JSObject a);
+
+    @InjectedBy(JSNativeInjector.class)
+    @PluggableDependency(JSNativeInjector.class)
+    public static native JSObject construct(JSObject cls, JSObject a, JSObject b);
+
+    @InjectedBy(JSNativeInjector.class)
+    @PluggableDependency(JSNativeInjector.class)
+    public static native JSObject construct(JSObject cls, JSObject a, JSObject b, JSObject c);
+
+    @InjectedBy(JSNativeInjector.class)
+    @PluggableDependency(JSNativeInjector.class)
+    public static native JSObject construct(JSObject cls, JSObject a, JSObject b, JSObject c, JSObject d);
+
+    @InjectedBy(JSNativeInjector.class)
+    @PluggableDependency(JSNativeInjector.class)
+    public static native JSObject construct(JSObject cls, JSObject a, JSObject b, JSObject c, JSObject d, JSObject e);
+
+    @InjectedBy(JSNativeInjector.class)
+    @PluggableDependency(JSNativeInjector.class)
+    public static native JSObject construct(JSObject cls, JSObject a, JSObject b, JSObject c, JSObject d, JSObject e,
+            JSObject f);
+
+    @InjectedBy(JSNativeInjector.class)
+    @PluggableDependency(JSNativeInjector.class)
+    public static native JSObject construct(JSObject cls, JSObject a, JSObject b, JSObject c, JSObject d, JSObject e,
+            JSObject f, JSObject g);
+
+    @InjectedBy(JSNativeInjector.class)
+    @PluggableDependency(JSNativeInjector.class)
+    public static native JSObject construct(JSObject cls, JSObject a, JSObject b, JSObject c, JSObject d, JSObject e,
+            JSObject f, JSObject g, JSObject h);
+
+    @InjectedBy(JSNativeInjector.class)
+    @PluggableDependency(JSNativeInjector.class)
+    public static native JSObject construct(JSObject cls, JSObject a, JSObject b, JSObject c, JSObject d, JSObject e,
+            JSObject f, JSObject g, JSObject h, JSObject i);
+
+    @InjectedBy(JSNativeInjector.class)
+    @PluggableDependency(JSNativeInjector.class)
+    public static native JSObject construct(JSObject cls, JSObject a, JSObject b, JSObject c, JSObject d, JSObject e,
+            JSObject f, JSObject g, JSObject h, JSObject i, JSObject j);
+
+    @InjectedBy(JSNativeInjector.class)
+    @PluggableDependency(JSNativeInjector.class)
+    public static native JSObject construct(JSObject cls, JSObject a, JSObject b, JSObject c, JSObject d, JSObject e,
+            JSObject f, JSObject g, JSObject h, JSObject i, JSObject j, JSObject k);
+
+    @InjectedBy(JSNativeInjector.class)
+    @PluggableDependency(JSNativeInjector.class)
+    public static native JSObject construct(JSObject cls, JSObject a, JSObject b, JSObject c, JSObject d, JSObject e,
+            JSObject f, JSObject g, JSObject h, JSObject i, JSObject j, JSObject k, JSObject l);
+
+    @InjectedBy(JSNativeInjector.class)
+    @PluggableDependency(JSNativeInjector.class)
+    public static native JSObject construct(JSObject cls, JSObject a, JSObject b, JSObject c, JSObject d, JSObject e,
+            JSObject f, JSObject g, JSObject h, JSObject i, JSObject j, JSObject k, JSObject l, JSObject m);
+
+
     @InjectedBy(JSNativeInjector.class)
     @JSBody(params = { "instance", "index" }, script = "return instance[index];")
     public static native JSObject get(JSObject instance, JSObject index);
@@ -541,4 +606,12 @@ final class JS {
     @GeneratedBy(JSNativeGenerator.class)
     @PluggableDependency(JSNativeInjector.class)
     public static native JSObject functionAsObject(JSObject instance, JSObject property);
+
+    @InjectedBy(JSNativeInjector.class)
+    @NoSideEffects
+    public static native JSObject global(String name);
+
+    @InjectedBy(JSNativeInjector.class)
+    @NoSideEffects
+    public static native JSObject importModule(String name);
 }
diff --git a/jso/impl/src/main/java/org/teavm/jso/impl/JSAliasRenderer.java b/jso/impl/src/main/java/org/teavm/jso/impl/JSAliasRenderer.java
index 01c7678b0..4b65aa810 100644
--- a/jso/impl/src/main/java/org/teavm/jso/impl/JSAliasRenderer.java
+++ b/jso/impl/src/main/java/org/teavm/jso/impl/JSAliasRenderer.java
@@ -239,7 +239,7 @@ class JSAliasRenderer implements RendererListener, VirtualMethodContributor {
                 return true;
             }
             for (var method : cls.getMethods()) {
-                if (!method.hasModifier(ElementModifier.STATIC) && getPublicAlias(method) != null) {
+                if (getPublicAlias(method) != null) {
                     return true;
                 }
             }
diff --git a/jso/impl/src/main/java/org/teavm/jso/impl/JSAnnotationCache.java b/jso/impl/src/main/java/org/teavm/jso/impl/JSAnnotationCache.java
new file mode 100644
index 000000000..b6ca2ba6a
--- /dev/null
+++ b/jso/impl/src/main/java/org/teavm/jso/impl/JSAnnotationCache.java
@@ -0,0 +1,128 @@
+/*
+ *  Copyright 2024 Alexey Andreev.
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.teavm.jso.impl;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import org.teavm.diagnostics.Diagnostics;
+import org.teavm.model.AccessLevel;
+import org.teavm.model.CallLocation;
+import org.teavm.model.ClassReaderSource;
+import org.teavm.model.ElementModifier;
+import org.teavm.model.MethodReader;
+import org.teavm.model.MethodReference;
+
+abstract class JSAnnotationCache<T> {
+    private ClassReaderSource classes;
+    protected Diagnostics diagnostics;
+    private Map<MethodReference, Value<T>> data = new HashMap<>();
+
+    JSAnnotationCache(ClassReaderSource classes, Diagnostics diagnostics) {
+        this.classes = classes;
+        this.diagnostics = diagnostics;
+    }
+
+    T get(MethodReference methodReference, CallLocation location) {
+        var result = getValue(methodReference, location);
+        return result != null ? result.annotation : null;
+    }
+
+    private Value<T> getValue(MethodReference methodReference, CallLocation location) {
+        var result = data.get(methodReference);
+        if (result == null) {
+            result = extract(methodReference, location);
+            data.put(methodReference, result);
+        }
+        return result;
+    }
+
+    private Value<T> extract(MethodReference methodReference, CallLocation location) {
+        var cls = classes.get(methodReference.getClassName());
+        if (cls == null) {
+            return new Value<>(null, methodReference);
+        }
+        var method = cls.getMethod(methodReference.getDescriptor());
+        if (method == null || method.hasModifier(ElementModifier.STATIC)
+                || method.getLevel() == AccessLevel.PRIVATE) {
+            for (var candidateMethod : cls.getMethods()) {
+                if (candidateMethod.getName().equals(methodReference.getName())
+                        && !candidateMethod.hasModifier(ElementModifier.STATIC)
+                        && !candidateMethod.hasModifier(ElementModifier.FINAL)
+                        && candidateMethod.getLevel() != AccessLevel.PRIVATE
+                        && Arrays.equals(candidateMethod.getParameterTypes(), methodReference.getParameterTypes())) {
+                    method = candidateMethod;
+                    break;
+                }
+            }
+        }
+
+        if (method != null) {
+            methodReference = method.getReference();
+            var annotation = take(method, location);
+            if (annotation != null) {
+                return new Value<>(annotation, methodReference);
+            }
+        }
+
+        var candidates = new HashMap<MethodReference, T>();
+        if (cls.getParent() != null) {
+            var value = getValue(new MethodReference(cls.getParent(), methodReference.getDescriptor()), location);
+            if (value.annotation != null) {
+                candidates.put(value.source, value.annotation);
+            }
+        }
+        for (var itf : cls.getInterfaces()) {
+            var value = getValue(new MethodReference(itf, methodReference.getDescriptor()), location);
+            if (value != null) {
+                candidates.put(value.source, value.annotation);
+            }
+        }
+        if (candidates.isEmpty()) {
+            return new Value<>(null, methodReference);
+        }
+        if (candidates.size() == 1) {
+            var entry = candidates.entrySet().iterator().next();
+            return new Value<>(entry.getValue(), entry.getKey());
+        }
+
+        T annot = null;
+        MethodReference lastMethod = null;
+        for (var entry : candidates.entrySet()) {
+            if (annot != null && !annot.equals(entry.getValue())) {
+                diagnostics.error(location, "Method '{{m0}}' has inconsistent JS annotations from overridden "
+                        + "methods '{{m1}}' and '{{m2}}', so it should be annotated explicitly",
+                        methodReference, lastMethod, entry.getKey());
+                return new Value<>(null, methodReference);
+            }
+            annot = entry.getValue();
+            lastMethod = entry.getKey();
+        }
+        return new Value<>(annot, methodReference);
+    }
+
+    protected abstract T take(MethodReader method, CallLocation location);
+
+    private static class Value<T> {
+        final T annotation;
+        final MethodReference source;
+
+        Value(T annotation, MethodReference source) {
+            this.annotation = annotation;
+            this.source = source;
+        }
+    }
+}
diff --git a/jso/impl/src/main/java/org/teavm/jso/impl/JSClassProcessor.java b/jso/impl/src/main/java/org/teavm/jso/impl/JSClassProcessor.java
index b7787ad97..461d00dea 100644
--- a/jso/impl/src/main/java/org/teavm/jso/impl/JSClassProcessor.java
+++ b/jso/impl/src/main/java/org/teavm/jso/impl/JSClassProcessor.java
@@ -18,6 +18,7 @@ package org.teavm.jso.impl;
 import java.io.IOException;
 import java.io.StringReader;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
@@ -35,10 +36,7 @@ import org.teavm.interop.NoSideEffects;
 import org.teavm.jso.JSBody;
 import org.teavm.jso.JSByRef;
 import org.teavm.jso.JSFunctor;
-import org.teavm.jso.JSIndexer;
-import org.teavm.jso.JSMethod;
 import org.teavm.jso.JSObject;
-import org.teavm.jso.JSProperty;
 import org.teavm.model.AnnotationContainerReader;
 import org.teavm.model.AnnotationHolder;
 import org.teavm.model.AnnotationReader;
@@ -63,6 +61,7 @@ import org.teavm.model.instructions.AssignInstruction;
 import org.teavm.model.instructions.CastInstruction;
 import org.teavm.model.instructions.ClassConstantInstruction;
 import org.teavm.model.instructions.ConstructArrayInstruction;
+import org.teavm.model.instructions.ConstructInstruction;
 import org.teavm.model.instructions.ExitInstruction;
 import org.teavm.model.instructions.GetElementInstruction;
 import org.teavm.model.instructions.IntegerConstantInstruction;
@@ -91,6 +90,8 @@ class JSClassProcessor {
     private final JSBodyRepository repository;
     private final JavaInvocationProcessor javaInvocationProcessor;
     private Program program;
+    private int[] variableAliases;
+    private boolean[] nativeConstructedObjects;
     private JSTypeInference types;
     private final List<Instruction> replacement = new ArrayList<>();
     private final JSTypeHelper typeHelper;
@@ -98,6 +99,7 @@ class JSClassProcessor {
     private final Map<MethodReference, MethodReader> overriddenMethodCache = new HashMap<>();
     private JSValueMarshaller marshaller;
     private IncrementalDependencyRegistration incrementalCache;
+    private JSImportAnnotationCache annotationCache;
 
     JSClassProcessor(ClassReaderSource classSource, JSTypeHelper typeHelper, JSBodyRepository repository,
             Diagnostics diagnostics, IncrementalDependencyRegistration incrementalCache) {
@@ -107,6 +109,8 @@ class JSClassProcessor {
         this.diagnostics = diagnostics;
         this.incrementalCache = incrementalCache;
         javaInvocationProcessor = new JavaInvocationProcessor(typeHelper, repository, classSource, diagnostics);
+
+        annotationCache = new JSImportAnnotationCache(classSource, diagnostics);
     }
 
     public ClassReaderSource getClassSource() {
@@ -220,6 +224,48 @@ class JSClassProcessor {
     private void setCurrentProgram(Program program) {
         this.program = program;
         marshaller = new JSValueMarshaller(diagnostics, typeHelper, classSource, program, replacement);
+        findVariableAliases();
+    }
+
+    private void findVariableAliases() {
+        if (program == null) {
+            return;
+        }
+        variableAliases = new int[program.variableCount()];
+        nativeConstructedObjects = new boolean[program.variableCount()];
+        var resolved = new boolean[program.variableCount()];
+        Arrays.fill(resolved, true);
+        for (var i = 0; i < variableAliases.length; ++i) {
+            variableAliases[i] = i;
+        }
+        for (var block : program.getBasicBlocks()) {
+            for (var instruction : block) {
+                if (instruction instanceof AssignInstruction) {
+                    var assign = (AssignInstruction) instruction;
+                    var from = assign.getAssignee().getIndex();
+                    var to = assign.getReceiver().getIndex();
+                    variableAliases[to] = from;
+                    resolved[assign.getAssignee().getIndex()] = true;
+                } else if (instruction instanceof ConstructInstruction) {
+                    var construct = (ConstructInstruction) instruction;
+                    if (typeHelper.isJavaScriptClass(construct.getType())) {
+                        nativeConstructedObjects[construct.getReceiver().getIndex()] = true;
+                    }
+                }
+            }
+        }
+        for (var i = 0; i < variableAliases.length; ++i) {
+            getVariableAlias(i, resolved);
+        }
+    }
+
+    private int getVariableAlias(int index, boolean[] resolved) {
+        if (resolved[index]) {
+            return variableAliases[index];
+        }
+        resolved[index] = true;
+        variableAliases[index] = getVariableAlias(variableAliases[index], resolved);
+        return variableAliases[index];
     }
 
     void processProgram(MethodHolder methodToProcess) {
@@ -266,9 +312,28 @@ class JSClassProcessor {
                             methodToProcess.getResultType()));
                 } else if (insn instanceof ClassConstantInstruction) {
                     processClassConstant((ClassConstantInstruction) insn);
+                } else if (insn instanceof ConstructInstruction) {
+                    processConstructObject((ConstructInstruction) insn);
+                } else if (insn instanceof AssignInstruction) {
+                    var assign = (AssignInstruction) insn;
+                    var index = variableAliases[assign.getReceiver().getIndex()];
+                    if (nativeConstructedObjects[index]) {
+                        assign.delete();
+                    }
                 }
             }
         }
+
+        var varMapper = new InstructionVariableMapper(v -> {
+            if (v.getIndex() < nativeConstructedObjects.length
+                    && nativeConstructedObjects[variableAliases[v.getIndex()]]) {
+                return program.variableAt(variableAliases[v.getIndex()]);
+            }
+            return v;
+        });
+        for (var block : program.getBasicBlocks()) {
+            varMapper.apply(block);
+        }
     }
 
     private void processInvokeArgs(InvokeInstruction invoke, MethodReader methodToInvoke) {
@@ -346,6 +411,12 @@ class JSClassProcessor {
         insn.setConstant(processType(insn.getConstant()));
     }
 
+    private void processConstructObject(ConstructInstruction insn) {
+        if (nativeConstructedObjects[insn.getReceiver().getIndex()]) {
+            insn.delete();
+        }
+    }
+
     private ValueType processType(ValueType type) {
         return processType(typeHelper, type);
     }
@@ -485,11 +556,21 @@ class JSClassProcessor {
             return false;
         }
 
-        if (method.hasModifier(ElementModifier.STATIC)) {
-            return false;
+        if (method.getName().equals("<init>")) {
+            return processConstructor(method, callLocation, invoke);
         }
 
+        var isStatic = method.hasModifier(ElementModifier.STATIC);
+        if (isStatic) {
+            switch (method.getOwnerName()) {
+                case "org.teavm.platform.PlatformQueue":
+                    return false;
+            }
+        }
         if (method.getProgram() != null && method.getProgram().basicBlockCount() > 0) {
+            if (isStatic) {
+                return false;
+            }
             MethodReader overridden = getOverriddenMethod(method);
             if (overridden != null) {
                 diagnostics.error(callLocation, "JS final method {{m0}} overrides {{m1}}. "
@@ -511,13 +592,18 @@ class JSClassProcessor {
             return false;
         }
 
-        if (method.getAnnotations().get(JSProperty.class.getName()) != null) {
-            return processProperty(method, callLocation, invoke);
-        } else if (method.getAnnotations().get(JSIndexer.class.getName()) != null) {
-            return processIndexer(method, callLocation, invoke);
-        } else {
-            return processMethod(method, callLocation, invoke);
+        var annot = annotationCache.get(method.getReference(), callLocation);
+        if (annot != null) {
+            switch (annot.kind) {
+                case PROPERTY:
+                    return processProperty(method, annot.name, callLocation, invoke);
+                case INDEXER:
+                    return processIndexer(method, callLocation, invoke);
+                case METHOD:
+                    return processMethod(method, annot.name, callLocation, invoke);
+            }
         }
+        return processMethod(method, null, callLocation, invoke);
     }
 
     private boolean processJSBodyInvocation(MethodReader method, CallLocation callLocation, InvokeInstruction invoke,
@@ -577,16 +663,17 @@ class JSClassProcessor {
         return true;
     }
 
-    private boolean processProperty(MethodReader method, CallLocation callLocation, InvokeInstruction invoke) {
+    private boolean processProperty(MethodReader method, String suggestedName, CallLocation callLocation,
+            InvokeInstruction invoke) {
         boolean pure = method.getAnnotations().get(NO_SIDE_EFFECTS) != null;
-        if (isProperGetter(method)) {
-            String propertyName = extractSuggestedPropertyName(method);
+        if (isProperGetter(method, suggestedName)) {
+            var propertyName = suggestedName;
             if (propertyName == null) {
                 propertyName = method.getName().charAt(0) == 'i' ? cutPrefix(method.getName(), 2)
                         : cutPrefix(method.getName(), 3);
             }
             Variable result = invoke.getReceiver() != null ? program.createVariable() : null;
-            addPropertyGet(propertyName, invoke.getInstance(), result, invoke.getLocation(), pure);
+            addPropertyGet(propertyName, getCallTarget(invoke), result, invoke.getLocation(), pure);
             if (result != null) {
                 result = marshaller.unwrapReturnValue(callLocation, result, method.getResultType(), false,
                         canBeOnlyJava(invoke.getReceiver()));
@@ -594,15 +681,15 @@ class JSClassProcessor {
             }
             return true;
         }
-        if (isProperSetter(method)) {
-            String propertyName = extractSuggestedPropertyName(method);
+        if (isProperSetter(method, suggestedName)) {
+            var propertyName = suggestedName;
             if (propertyName == null) {
                 propertyName = cutPrefix(method.getName(), 3);
             }
             var value = invoke.getArguments().get(0);
             value = marshaller.wrapArgument(callLocation, value,
                     method.parameterType(0), types.typeOf(value), false);
-            addPropertySet(propertyName, invoke.getInstance(), value, invoke.getLocation(), pure);
+            addPropertySet(propertyName, getCallTarget(invoke), value, invoke.getLocation(), pure);
             return true;
         }
         diagnostics.error(callLocation, "Method {{m0}} is not a proper native JavaScript property "
@@ -610,17 +697,11 @@ class JSClassProcessor {
         return false;
     }
 
-    private String extractSuggestedPropertyName(MethodReader method) {
-        AnnotationReader annot = method.getAnnotations().get(JSProperty.class.getName());
-        AnnotationValue value = annot.getValue("value");
-        return value != null ? value.getString() : null;
-    }
-
     private boolean processIndexer(MethodReader method, CallLocation callLocation, InvokeInstruction invoke) {
         if (isProperGetIndexer(method.getDescriptor())) {
             Variable result = invoke.getReceiver() != null ? program.createVariable() : null;
             var index = invoke.getArguments().get(0);
-            addIndexerGet(invoke.getInstance(), marshaller.wrapArgument(callLocation, index,
+            addIndexerGet(getCallTarget(invoke), marshaller.wrapArgument(callLocation, index,
                     method.parameterType(0), types.typeOf(index), false), result, invoke.getLocation());
             if (result != null) {
                 result = marshaller.unwrapReturnValue(callLocation, result, method.getResultType(), false,
@@ -635,7 +716,7 @@ class JSClassProcessor {
             var value = invoke.getArguments().get(1);
             value = marshaller.wrapArgument(callLocation, value, method.parameterType(1),
                     types.typeOf(value), false);
-            addIndexerSet(invoke.getInstance(), index, value, invoke.getLocation());
+            addIndexerSet(getCallTarget(invoke), index, value, invoke.getLocation());
             return true;
         }
         diagnostics.error(callLocation, "Method {{m0}} is not a proper native JavaScript indexer "
@@ -678,17 +759,11 @@ class JSClassProcessor {
         return type != JSType.JS && type != JSType.MIXED;
     }
 
-    private boolean processMethod(MethodReader method, CallLocation callLocation, InvokeInstruction invoke) {
-        String name = method.getName();
-
-        AnnotationReader methodAnnot = method.getAnnotations().get(JSMethod.class.getName());
-        if (methodAnnot != null) {
-            AnnotationValue redefinedMethodName = methodAnnot.getValue("value");
-            if (redefinedMethodName != null) {
-                name = redefinedMethodName.getString();
-            }
+    private boolean processMethod(MethodReader method, String name, CallLocation callLocation,
+            InvokeInstruction invoke) {
+        if (name == null) {
+            name = method.getName();
         }
-
         boolean[] byRefParams = new boolean[method.parameterCount() + 1];
         if (!validateSignature(method, callLocation, byRefParams)) {
             return false;
@@ -700,7 +775,7 @@ class JSClassProcessor {
         newInvoke.setType(InvocationType.SPECIAL);
         newInvoke.setReceiver(result);
         List<Variable> newArguments = new ArrayList<>();
-        newArguments.add(invoke.getInstance());
+        newArguments.add(getCallTarget(invoke));
         newArguments.add(marshaller.addStringWrap(marshaller.addString(name, invoke.getLocation()),
                 invoke.getLocation()));
         newInvoke.setLocation(invoke.getLocation());
@@ -721,6 +796,38 @@ class JSClassProcessor {
         return true;
     }
 
+    private Variable getCallTarget(InvokeInstruction invoke) {
+        return invoke.getInstance() != null
+                ? invoke.getInstance()
+                : marshaller.classRef(invoke.getMethod().getClassName(), invoke.getLocation());
+    }
+
+    private boolean processConstructor(MethodReader method, CallLocation callLocation, InvokeInstruction invoke) {
+        var byRefParams = new boolean[method.parameterCount() + 1];
+        if (!validateSignature(method, callLocation, byRefParams)) {
+            return false;
+        }
+
+        var result = program.variableAt(variableAliases[invoke.getInstance().getIndex()]);
+        var newInvoke = new InvokeInstruction();
+        newInvoke.setMethod(JSMethods.construct(method.parameterCount()));
+        newInvoke.setType(InvocationType.SPECIAL);
+        newInvoke.setReceiver(result);
+        var newArguments = new ArrayList<Variable>();
+        newArguments.add(marshaller.classRef(invoke.getMethod().getClassName(), invoke.getLocation()));
+        newInvoke.setLocation(invoke.getLocation());
+        for (int i = 0; i < invoke.getArguments().size(); ++i) {
+            var arg = invoke.getArguments().get(i);
+            arg = marshaller.wrapArgument(callLocation, arg,
+                    method.parameterType(i), types.typeOf(arg), byRefParams[i]);
+            newArguments.add(arg);
+        }
+        newInvoke.setArguments(newArguments.toArray(new Variable[0]));
+        replacement.add(newInvoke);
+
+        return true;
+    }
+
     private void requireJSBody(Diagnostics diagnostics, MethodReader methodToProcess) {
         if (!repository.processedMethods.add(methodToProcess.getReference())) {
             return;
@@ -997,11 +1104,11 @@ class JSClassProcessor {
         return null;
     }
 
-    private boolean isProperGetter(MethodReader method) {
+    private boolean isProperGetter(MethodReader method, String suggestedName) {
         if (method.parameterCount() > 0 || !typeHelper.isSupportedType(method.getResultType())) {
             return false;
         }
-        if (extractSuggestedPropertyName(method) != null) {
+        if (suggestedName != null) {
             return true;
         }
 
@@ -1013,13 +1120,13 @@ class JSClassProcessor {
         return isProperPrefix(method.getName(), "get");
     }
 
-    private boolean isProperSetter(MethodReader method) {
+    private boolean isProperSetter(MethodReader method, String suggestedName) {
         if (method.parameterCount() != 1 || !typeHelper.isSupportedType(method.parameterType(0))
                 || method.getResultType() != ValueType.VOID) {
             return false;
         }
 
-        return extractSuggestedPropertyName(method) != null || isProperPrefix(method.getName(), "set");
+        return suggestedName != null || isProperPrefix(method.getName(), "set");
     }
 
     private boolean isProperPrefix(String name, String prefix) {
diff --git a/jso/impl/src/main/java/org/teavm/jso/impl/JSImportAnnotationCache.java b/jso/impl/src/main/java/org/teavm/jso/impl/JSImportAnnotationCache.java
new file mode 100644
index 000000000..70cdd4d75
--- /dev/null
+++ b/jso/impl/src/main/java/org/teavm/jso/impl/JSImportAnnotationCache.java
@@ -0,0 +1,66 @@
+/*
+ *  Copyright 2024 Alexey Andreev.
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.teavm.jso.impl;
+
+import java.util.Arrays;
+import org.teavm.diagnostics.Diagnostics;
+import org.teavm.jso.JSIndexer;
+import org.teavm.jso.JSMethod;
+import org.teavm.jso.JSProperty;
+import org.teavm.model.AnnotationReader;
+import org.teavm.model.CallLocation;
+import org.teavm.model.ClassReaderSource;
+import org.teavm.model.MethodReader;
+
+class JSImportAnnotationCache extends JSAnnotationCache<JSImportDescriptor> {
+    JSImportAnnotationCache(ClassReaderSource classes, Diagnostics diagnostics) {
+        super(classes, diagnostics);
+    }
+
+    @Override
+    protected JSImportDescriptor take(MethodReader method, CallLocation location) {
+        var propertyAnnot = method.getAnnotations().get(JSProperty.class.getName());
+        var indexerAnnot = method.getAnnotations().get(JSIndexer.class.getName());
+        var methodAnnot = method.getAnnotations().get(JSMethod.class.getName());
+        var found = false;
+        for (var annot : Arrays.asList(propertyAnnot, indexerAnnot, methodAnnot)) {
+            if (annot != null) {
+                if (!found) {
+                    found = true;
+                } else {
+                    diagnostics.error(location, "@JSProperty, @JSIndexer and @JSMethod are mutually exclusive "
+                            + "and can't appear simultaneously on {{m}}", method.getReference());
+                    return null;
+                }
+            }
+        }
+        if (propertyAnnot != null) {
+            return new JSImportDescriptor(JSImportKind.PROPERTY, extractValue(propertyAnnot));
+        }
+        if (indexerAnnot != null) {
+            return new JSImportDescriptor(JSImportKind.INDEXER, null);
+        }
+        if (methodAnnot != null) {
+            return new JSImportDescriptor(JSImportKind.METHOD, extractValue(methodAnnot));
+        }
+        return null;
+    }
+
+    private String extractValue(AnnotationReader annotation) {
+        var value = annotation.getValue("value");
+        return value != null ? value.getString() : null;
+    }
+}
diff --git a/jso/impl/src/main/java/org/teavm/jso/impl/JSImportDescriptor.java b/jso/impl/src/main/java/org/teavm/jso/impl/JSImportDescriptor.java
new file mode 100644
index 000000000..28e169780
--- /dev/null
+++ b/jso/impl/src/main/java/org/teavm/jso/impl/JSImportDescriptor.java
@@ -0,0 +1,45 @@
+/*
+ *  Copyright 2024 Alexey Andreev.
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.teavm.jso.impl;
+
+import java.util.Objects;
+
+class JSImportDescriptor {
+    final JSImportKind kind;
+    final String name;
+
+    JSImportDescriptor(JSImportKind kind, String name) {
+        this.kind = kind;
+        this.name = name;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (this == o) {
+            return true;
+        }
+        if (!(o instanceof JSImportDescriptor)) {
+            return false;
+        }
+        var that = (JSImportDescriptor) o;
+        return kind == that.kind && Objects.equals(name, that.name);
+    }
+
+    @Override
+    public int hashCode() {
+        return Objects.hash(kind, name);
+    }
+}
diff --git a/jso/impl/src/main/java/org/teavm/jso/impl/JSImportKind.java b/jso/impl/src/main/java/org/teavm/jso/impl/JSImportKind.java
new file mode 100644
index 000000000..71b0c4be9
--- /dev/null
+++ b/jso/impl/src/main/java/org/teavm/jso/impl/JSImportKind.java
@@ -0,0 +1,22 @@
+/*
+ *  Copyright 2024 Alexey Andreev.
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.teavm.jso.impl;
+
+enum JSImportKind {
+    PROPERTY,
+    INDEXER,
+    METHOD
+}
diff --git a/jso/impl/src/main/java/org/teavm/jso/impl/JSMethods.java b/jso/impl/src/main/java/org/teavm/jso/impl/JSMethods.java
index 542ca2cdd..d0374c378 100644
--- a/jso/impl/src/main/java/org/teavm/jso/impl/JSMethods.java
+++ b/jso/impl/src/main/java/org/teavm/jso/impl/JSMethods.java
@@ -114,15 +114,24 @@ final class JSMethods {
     public static final MethodReference FUNCTION_AS_OBJECT = new MethodReference(JS.class, "functionAsObject",
             JSObject.class, JSObject.class, JSObject.class);
 
+    public static final MethodReference GLOBAL = new MethodReference(JS.class, "global", String.class, JSObject.class);
+    public static final MethodReference IMPORT_MODULE = new MethodReference(JS.class, "importModule",
+            String.class, JSObject.class);
+
     public static final ValueType JS_OBJECT = ValueType.object(JSObject.class.getName());
     public static final ValueType JS_ARRAY = ValueType.object(JSArray.class.getName());
     private static final MethodReference[] INVOKE_METHODS = new MethodReference[13];
+    private static final MethodReference[] CONSTRUCT_METHODS = new MethodReference[13];
 
     static {
         for (int i = 0; i < INVOKE_METHODS.length; ++i) {
-            ValueType[] signature = new ValueType[i + 3];
+            var signature = new ValueType[i + 3];
             Arrays.fill(signature, JS_OBJECT);
             INVOKE_METHODS[i] = new MethodReference(JS.class.getName(), "invoke", signature);
+
+            var constructSignature = new ValueType[i + 2];
+            Arrays.fill(constructSignature, JS_OBJECT);
+            CONSTRUCT_METHODS[i] = new MethodReference(JS.class.getName(), "construct", constructSignature);
         }
     }
 
@@ -132,4 +141,8 @@ final class JSMethods {
     public static MethodReference invoke(int parameterCount) {
         return INVOKE_METHODS[parameterCount];
     }
+
+    public static MethodReference construct(int parameterCount) {
+        return CONSTRUCT_METHODS[parameterCount];
+    }
 }
diff --git a/jso/impl/src/main/java/org/teavm/jso/impl/JSNativeInjector.java b/jso/impl/src/main/java/org/teavm/jso/impl/JSNativeInjector.java
index cf000048f..fb621fa4f 100644
--- a/jso/impl/src/main/java/org/teavm/jso/impl/JSNativeInjector.java
+++ b/jso/impl/src/main/java/org/teavm/jso/impl/JSNativeInjector.java
@@ -71,21 +71,20 @@ public class JSNativeInjector implements Injector, DependencyPlugin {
                 }
                 writer.append(')');
                 break;
-            case "instantiate":
+            case "construct":
                 if (context.getPrecedence().ordinal() >= Precedence.FUNCTION_CALL.ordinal()) {
                     writer.append("(");
                 }
                 writer.append("new ");
                 context.writeExpr(context.getArgument(0), Precedence.GROUPING);
-                renderProperty(context.getArgument(1), context);
-                writer.append("(");
-                for (int i = 2; i < context.argumentCount(); ++i) {
-                    if (i > 2) {
+                writer.append('(');
+                for (int i = 1; i < context.argumentCount(); ++i) {
+                    if (i > 1) {
                         writer.append(',').ws();
                     }
                     context.writeExpr(context.getArgument(i), Precedence.min());
                 }
-                writer.append(")");
+                writer.append(')');
                 if (context.getPrecedence().ordinal() >= Precedence.FUNCTION_CALL.ordinal()) {
                     writer.append(")");
                 }
@@ -152,6 +151,18 @@ public class JSNativeInjector implements Injector, DependencyPlugin {
             case "dataToArray":
                 dataToArray(context, "$rt_objcls");
                 break;
+            case "global": {
+                var cst = (ConstantExpr) context.getArgument(0);
+                var name = (String) cst.getValue();
+                writer.appendGlobal(name);
+                break;
+            }
+            case "importModule": {
+                var cst = (ConstantExpr) context.getArgument(0);
+                var name = (String) cst.getValue();
+                writer.appendFunction(context.importModule(name));
+                break;
+            }
 
             default:
                 if (methodRef.getName().startsWith("unwrap")) {
@@ -172,7 +183,7 @@ public class JSNativeInjector implements Injector, DependencyPlugin {
     public void methodReached(DependencyAgent agent, MethodDependency method) {
         switch (method.getReference().getName()) {
             case "invoke":
-            case "instantiate":
+            case "construct":
             case "function":
                 if (reachedFunctorMethods.add(method.getReference()) && !method.isMissing()) {
                     for (int i = 0; i < method.getReference().parameterCount(); ++i) {
diff --git a/jso/impl/src/main/java/org/teavm/jso/impl/JSObjectClassTransformer.java b/jso/impl/src/main/java/org/teavm/jso/impl/JSObjectClassTransformer.java
index dec9d8b6c..2cb7259d5 100644
--- a/jso/impl/src/main/java/org/teavm/jso/impl/JSObjectClassTransformer.java
+++ b/jso/impl/src/main/java/org/teavm/jso/impl/JSObjectClassTransformer.java
@@ -23,6 +23,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import org.teavm.diagnostics.Diagnostics;
+import org.teavm.jso.JSClass;
 import org.teavm.jso.JSExport;
 import org.teavm.jso.JSMethod;
 import org.teavm.jso.JSObject;
@@ -82,6 +83,11 @@ class JSObjectClassTransformer implements ClassHolderTransformer {
         }
         processor.createJSMethods(cls);
 
+        if (cls.hasModifier(ElementModifier.ABSTRACT)
+                || cls.getAnnotations().get(JSClass.class.getName()) != null && isJavaScriptClass(cls)) {
+            return;
+        }
+
         MethodReference functorMethod = processor.isFunctor(cls.getName());
         if (functorMethod != null) {
             if (processor.isFunctor(cls.getParent()) != null) {
@@ -277,6 +283,18 @@ class JSObjectClassTransformer implements ClassHolderTransformer {
         }
     }
 
+    private boolean isJavaScriptClass(ClassReader cls) {
+        if (cls.getParent() != null && typeHelper.isJavaScriptClass(cls.getParent())) {
+            return true;
+        }
+        for (var itf : cls.getInterfaces()) {
+            if (typeHelper.isJavaScriptClass(itf)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
     private boolean addInterfaces(ExposedClass exposedCls, ClassReader cls) {
         boolean added = false;
         for (String ifaceName : cls.getInterfaces()) {
diff --git a/jso/impl/src/main/java/org/teavm/jso/impl/JSTypeHelper.java b/jso/impl/src/main/java/org/teavm/jso/impl/JSTypeHelper.java
index 5d6c759e0..5d497d52c 100644
--- a/jso/impl/src/main/java/org/teavm/jso/impl/JSTypeHelper.java
+++ b/jso/impl/src/main/java/org/teavm/jso/impl/JSTypeHelper.java
@@ -17,6 +17,7 @@ package org.teavm.jso.impl;
 
 import java.util.HashMap;
 import java.util.Map;
+import org.teavm.jso.JSClass;
 import org.teavm.jso.JSObject;
 import org.teavm.model.ClassReader;
 import org.teavm.model.ClassReaderSource;
@@ -43,15 +44,20 @@ class JSTypeHelper {
     }
 
     public boolean isJavaScriptImplementation(String className) {
-        return knownJavaScriptImplementations
-                .computeIfAbsent(className, k -> examineIfJavaScriptImplementation(className));
+        return knownJavaScriptImplementations.computeIfAbsent(className, k ->
+                examineIfJavaScriptImplementation(className));
     }
 
     private boolean examineIfJavaScriptClass(String className) {
         ClassReader cls = classSource.get(className);
-        if (cls == null || !(cls.hasModifier(ElementModifier.INTERFACE) || cls.hasModifier(ElementModifier.ABSTRACT))) {
+        if (cls == null) {
             return false;
         }
+        if (!(cls.hasModifier(ElementModifier.INTERFACE) || cls.hasModifier(ElementModifier.ABSTRACT))) {
+            if (cls.getAnnotations().get(JSClass.class.getName()) == null) {
+                return false;
+            }
+        }
         if (cls.getParent() != null) {
             if (isJavaScriptClass(cls.getParent())) {
                 return true;
@@ -65,7 +71,7 @@ class JSTypeHelper {
             return false;
         }
         ClassReader cls = classSource.get(className);
-        if (cls == null) {
+        if (cls == null || cls.getAnnotations().get(JSClass.class.getName()) != null) {
             return false;
         }
         if (cls.getParent() != null) {
diff --git a/jso/impl/src/main/java/org/teavm/jso/impl/JSValueMarshaller.java b/jso/impl/src/main/java/org/teavm/jso/impl/JSValueMarshaller.java
index 6c3812798..a8e8dcc6b 100644
--- a/jso/impl/src/main/java/org/teavm/jso/impl/JSValueMarshaller.java
+++ b/jso/impl/src/main/java/org/teavm/jso/impl/JSValueMarshaller.java
@@ -18,7 +18,9 @@ package org.teavm.jso.impl;
 import java.util.ArrayList;
 import java.util.List;
 import org.teavm.diagnostics.Diagnostics;
+import org.teavm.jso.JSClass;
 import org.teavm.jso.JSFunctor;
+import org.teavm.jso.JSModule;
 import org.teavm.jso.JSObject;
 import org.teavm.model.CallLocation;
 import org.teavm.model.ClassReader;
@@ -553,4 +555,90 @@ class JSValueMarshaller {
         replacement.add(nameInsn);
         return var;
     }
+
+    Variable classRef(String className, TextLocation location) {
+        String name = null;
+        String module = null;
+        var cls = classSource.get(className);
+        if (cls != null) {
+            name = cls.getSimpleName();
+            var jsExport = cls.getAnnotations().get(JSClass.class.getName());
+            if (jsExport != null) {
+                var nameValue = jsExport.getValue("name");
+                if (nameValue != null) {
+                    var nameValueString = nameValue.getString();
+                    if (!nameValueString.isEmpty()) {
+                        name = nameValueString;
+                    }
+                }
+            }
+            var jsModule = cls.getAnnotations().get(JSModule.class.getName());
+            if (jsModule != null) {
+                module = jsModule.getValue("value").getString();
+            }
+        }
+        if (name == null) {
+            name = cls.getName().substring(cls.getName().lastIndexOf('.') + 1);
+        }
+        return module != null ? moduleRef(module, name, location) : globalRef(name, location);
+    }
+
+    Variable globalRef(String name, TextLocation location) {
+        var nameInsn = new StringConstantInstruction();
+        nameInsn.setReceiver(program.createVariable());
+        nameInsn.setConstant(name);
+        nameInsn.setLocation(location);
+        replacement.add(nameInsn);
+
+        var invoke = new InvokeInstruction();
+        invoke.setType(InvocationType.SPECIAL);
+        invoke.setMethod(JSMethods.GLOBAL);
+        invoke.setArguments(nameInsn.getReceiver());
+        invoke.setReceiver(program.createVariable());
+        invoke.setLocation(location);
+        replacement.add(invoke);
+
+        return invoke.getReceiver();
+    }
+
+    Variable moduleRef(String module, String name, TextLocation location) {
+        var moduleNameInsn = new StringConstantInstruction();
+        moduleNameInsn.setReceiver(program.createVariable());
+        moduleNameInsn.setConstant(module);
+        moduleNameInsn.setLocation(location);
+        replacement.add(moduleNameInsn);
+
+        var invoke = new InvokeInstruction();
+        invoke.setType(InvocationType.SPECIAL);
+        invoke.setMethod(JSMethods.IMPORT_MODULE);
+        invoke.setArguments(moduleNameInsn.getReceiver());
+        invoke.setReceiver(program.createVariable());
+        invoke.setLocation(location);
+        replacement.add(invoke);
+
+        var nameInsn = new StringConstantInstruction();
+        nameInsn.setReceiver(program.createVariable());
+        nameInsn.setConstant(name);
+        nameInsn.setLocation(location);
+        replacement.add(nameInsn);
+
+        var wrapName = new InvokeInstruction();
+        wrapName.setType(InvocationType.SPECIAL);
+        wrapName.setMethod(referenceCache.getCached(new MethodReference(JS.class, "wrap",
+                String.class, JSObject.class)));
+        wrapName.setReceiver(program.createVariable());
+        wrapName.setArguments(nameInsn.getReceiver());
+        wrapName.setLocation(location);
+        replacement.add(wrapName);
+
+        var get = new InvokeInstruction();
+        get.setType(InvocationType.SPECIAL);
+        get.setMethod(JSMethods.GET_PURE);
+        get.setReceiver(program.createVariable());
+        get.setArguments(invoke.getReceiver(), wrapName.getReceiver());
+        get.setLocation(location);
+        replacement.add(get);
+
+        return get.getReceiver();
+    }
 }
diff --git a/jso/impl/src/main/java/org/teavm/jso/impl/JSWrapper.java b/jso/impl/src/main/java/org/teavm/jso/impl/JSWrapper.java
index bf81f91df..04d76feb8 100644
--- a/jso/impl/src/main/java/org/teavm/jso/impl/JSWrapper.java
+++ b/jso/impl/src/main/java/org/teavm/jso/impl/JSWrapper.java
@@ -28,13 +28,13 @@ import org.teavm.jso.core.JSWeakMap;
 import org.teavm.jso.core.JSWeakRef;
 
 public final class JSWrapper {
-    private static final JSWeakMap<JSObject, JSNumber> hashCodes = JSWeakMap.create();
+    private static final JSWeakMap<JSObject, JSNumber> hashCodes = new JSWeakMap<>();
     private static final JSWeakMap<JSObject, JSWeakRef<JSObject>> wrappers = JSWeakRef.isSupported()
-            ? JSWeakMap.create() : null;
+            ? new JSWeakMap<>() : null;
     private static final JSMap<JSString, JSWeakRef<JSObject>> stringWrappers = JSWeakRef.isSupported()
-            ? JSMap.create() : null;
+            ? new JSMap<>() : null;
     private static final JSMap<JSNumber, JSWeakRef<JSObject>> numberWrappers = JSWeakRef.isSupported()
-            ? JSMap.create() : null;
+            ? new JSMap<>() : null;
     private static JSWeakRef<JSObject> undefinedWrapper;
     private static final JSFinalizationRegistry stringFinalizationRegistry;
     private static final JSFinalizationRegistry numberFinalizationRegistry;
@@ -44,10 +44,10 @@ public final class JSWrapper {
 
     static {
         stringFinalizationRegistry = stringWrappers != null
-                ? JSFinalizationRegistry.create(token -> stringWrappers.delete((JSString) token))
+                ? new JSFinalizationRegistry(token -> stringWrappers.delete((JSString) token))
                 : null;
         numberFinalizationRegistry = numberWrappers != null
-                ? JSFinalizationRegistry.create(token -> numberWrappers.delete((JSNumber) token))
+                ? new JSFinalizationRegistry(token -> numberWrappers.delete((JSNumber) token))
                 : null;
     }
 
diff --git a/samples/hello/src/teavm/java/org/teavm/samples/hello/Client.java b/samples/hello/src/teavm/java/org/teavm/samples/hello/Client.java
index 4ce9fc6f5..b6c603c86 100644
--- a/samples/hello/src/teavm/java/org/teavm/samples/hello/Client.java
+++ b/samples/hello/src/teavm/java/org/teavm/samples/hello/Client.java
@@ -37,7 +37,7 @@ public final class Client {
     private static void sayHello() {
         helloButton.setDisabled(true);
         thinkingPanel.getStyle().setProperty("display", "");
-        var xhr = XMLHttpRequest.create();
+        var xhr = new XMLHttpRequest();
         xhr.onComplete(() -> receiveResponse(xhr.getResponseText()));
         xhr.open("GET", "hello");
         xhr.send();
diff --git a/samples/promise/src/main/java/org/teavm/samples/promise/PromiseExample.java b/samples/promise/src/main/java/org/teavm/samples/promise/PromiseExample.java
index 5b08d5049..cd2aa9834 100644
--- a/samples/promise/src/main/java/org/teavm/samples/promise/PromiseExample.java
+++ b/samples/promise/src/main/java/org/teavm/samples/promise/PromiseExample.java
@@ -103,7 +103,7 @@ public final class PromiseExample {
     }
 
     private static void runSimplePromise() {
-        var promise = JSPromise.create((resolve, reject) -> {
+        new JSPromise<>((resolve, reject) -> {
             report("Simple promise execution");
 
             report("Resolving with 'success'");
@@ -112,7 +112,7 @@ public final class PromiseExample {
     }
 
     private static void runComplexPromise() {
-        var promise = JSPromise.create((resolve, reject) -> {
+        new JSPromise<>((resolve, reject) -> {
             report("Complex promise execution");
 
             report("Resolving with 'step1'");
@@ -155,7 +155,7 @@ public final class PromiseExample {
         .flatThen(value -> {
             report("Resolved with '" + value + "'");
             report("... and resolve with new promise");
-            return JSPromise.create((resolve, reject) -> {
+            return new JSPromise<>((resolve, reject) -> {
                 report("Inner promise");
                 report("Reject with 'step from inner'");
                 reject.accept("step from inner");
@@ -176,7 +176,7 @@ public final class PromiseExample {
     }
 
     private static void runLongRunningPromise(Object lock) {
-        var promise = JSPromise.create((resolve, reject) -> {
+        var promise = new JSPromise<>((resolve, reject) -> {
             report("Long promise exection");
             report("Wait for a while...");
             Window.setTimeout(() -> {
@@ -193,7 +193,7 @@ public final class PromiseExample {
     }
 
     private static void combinePromises(Object lock) throws InterruptedException {
-        JSArray<JSPromise<String>> promises = JSArray.create(3);
+        var promises = new JSArray<JSPromise<String>>(3);
 
         report("Start 3 successful promises");
         promises.set(0, JSPromise.resolve("success1"));
@@ -245,7 +245,7 @@ public final class PromiseExample {
 
         var settledPromises = JSPromise.allSettled(promises);
         settledPromises.then(value -> {
-            report(Integer.toString(value.getLength()) + " promises settled to:");
+            report(value.getLength() + " promises settled to:");
             for (int i = 0; i < value.getLength(); ++i) {
                 var item = value.get(i);
                 var msg = "-- Promise " + i + " " + item.getStatus() + " with: ";
@@ -313,9 +313,9 @@ public final class PromiseExample {
         }
 
         report("Start 3 delayed promises");
-        promises.set(0, JSPromise.create((resolve, reject) -> Window.setTimeout(() -> resolve.accept("success1"), 200)));
-        promises.set(1, JSPromise.create((resolve, reject) -> Window.setTimeout(() -> reject.accept("failure1"), 100)));
-        promises.set(2, JSPromise.create((resolve, reject) -> Window.setTimeout(() -> resolve.accept("success3"), 50)));
+        promises.set(0, new JSPromise<>((resolve, reject) -> Window.setTimeout(() -> resolve.accept("success1"), 200)));
+        promises.set(1, new JSPromise<>((resolve, reject) -> Window.setTimeout(() -> reject.accept("failure1"), 100)));
+        promises.set(2, new JSPromise<>((resolve, reject) -> Window.setTimeout(() -> resolve.accept("success3"), 50)));
 
         anyPromise = JSPromise.race(promises);
         anyPromise.then(value -> {
@@ -337,9 +337,9 @@ public final class PromiseExample {
         }
 
         report("Start 3 delayed promises");
-        promises.set(0, JSPromise.create((resolve, reject) -> Window.setTimeout(() -> resolve.accept("success1"), 200)));
-        promises.set(1, JSPromise.create((resolve, reject) -> Window.setTimeout(() -> reject.accept("failure1"), 50)));
-        promises.set(2, JSPromise.create((resolve, reject) -> Window.setTimeout(() -> resolve.accept("success3"), 100)));
+        promises.set(0, new JSPromise<>((resolve, reject) -> Window.setTimeout(() -> resolve.accept("success1"), 200)));
+        promises.set(1, new JSPromise<>((resolve, reject) -> Window.setTimeout(() -> reject.accept("failure1"), 50)));
+        promises.set(2, new JSPromise<>((resolve, reject) -> Window.setTimeout(() -> resolve.accept("success3"), 100)));
 
         anyPromise = JSPromise.race(promises);
         anyPromise.then(value -> {
@@ -397,11 +397,11 @@ public final class PromiseExample {
         }
 
         report("Pass promise to native method");
-        handlePromise(JSPromise.create((resolve, reject) -> {
+        handlePromise(new JSPromise<>((resolve, reject) -> {
             resolve.accept(JSString.valueOf("Resolved from Java"));
         }));
 
-        handlePromise(JSPromise.create((resolve, reject) -> {
+        handlePromise(new JSPromise<>((resolve, reject) -> {
             reject.accept(JSString.valueOf("Rejected from Java"));
         }));
     }
diff --git a/samples/software3d/src/jvmMain/kotlin/org/teavm/samples/software3d/teavm/Controller.kt b/samples/software3d/src/jvmMain/kotlin/org/teavm/samples/software3d/teavm/Controller.kt
index 00835f2b1..55c2ed816 100644
--- a/samples/software3d/src/jvmMain/kotlin/org/teavm/samples/software3d/teavm/Controller.kt
+++ b/samples/software3d/src/jvmMain/kotlin/org/teavm/samples/software3d/teavm/Controller.kt
@@ -55,7 +55,7 @@ class Controller(
             WorkerType.KOTLIN_JS -> "kjs/software3d.js"
         }
         workers = (0 until tasks).map { index ->
-            Worker.create(scriptName).apply {
+            Worker(scriptName).apply {
                 postMessage(JSObjects.createWithoutProto<JSMapLike<JSObject>>().apply {
                     set("type", JSString.valueOf("init"))
                     set("width", JSNumber.valueOf(width))
@@ -118,8 +118,8 @@ class Controller(
     private fun displayBuffers(buffers: Array<out ArrayBuffer?>) {
         for (y in 0 until height) {
             val buffer = buffers[y % buffers.size]!!
-            val array = Uint8ClampedArray.create(buffer, width * 4 * (y / buffers.size), width * 4)
-            val imageData = ImageData.create(array, width, 1)
+            val array = Uint8ClampedArray(buffer, width * 4 * (y / buffers.size), width * 4)
+            val imageData = ImageData(array, width, 1)
             context.putImageData(imageData, 0.0, y.toDouble())
         }
     }
diff --git a/samples/web-apis/src/main/java/org/teavm/samples/webapis/Storage.java b/samples/web-apis/src/main/java/org/teavm/samples/webapis/Storage.java
index 8e568b445..4825c942f 100644
--- a/samples/web-apis/src/main/java/org/teavm/samples/webapis/Storage.java
+++ b/samples/web-apis/src/main/java/org/teavm/samples/webapis/Storage.java
@@ -41,7 +41,7 @@ public final class Storage {
             var key = document.getElementById("key").<HTMLInputElement>cast().getValue();
             var value = document.getElementById("value").<HTMLInputElement>cast().getValue();
 
-            if (key != null && key.length() > 0 && value != null && value.length() > 0) {
+            if (key != null && !key.isEmpty() && value != null && !value.isEmpty()) {
                 storage.setItem(key, value);
                 draw();
             }
@@ -49,7 +49,7 @@ public final class Storage {
         HTMLButtonElement deleteButton = document.getElementById("delete-button").cast();
         deleteButton.listenClick(e -> {
             String key = document.getElementById("key").<HTMLInputElement>cast().getValue();
-            if (key != null && key.length() > 0) {
+            if (key != null && !key.isEmpty()) {
                 storage.removeItem(key);
                 draw();
             }
diff --git a/tests/src/test/java/org/teavm/jso/test/ClassWithConstructor.java b/tests/src/test/java/org/teavm/jso/test/ClassWithConstructor.java
new file mode 100644
index 000000000..e2741717a
--- /dev/null
+++ b/tests/src/test/java/org/teavm/jso/test/ClassWithConstructor.java
@@ -0,0 +1,36 @@
+/*
+ *  Copyright 2024 Alexey Andreev.
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.teavm.jso.test;
+
+import org.teavm.jso.JSClass;
+import org.teavm.jso.JSObject;
+import org.teavm.jso.JSProperty;
+
+@JSClass
+public class ClassWithConstructor implements JSObject {
+    public ClassWithConstructor(int foo) {
+    }
+
+    public ClassWithConstructor() {
+    }
+
+    @JSProperty
+    public native int getFoo();
+
+    public native String bar();
+
+    public static native String staticMethod();
+}
diff --git a/tests/src/test/java/org/teavm/jso/test/ClassWithConstructorInModule.java b/tests/src/test/java/org/teavm/jso/test/ClassWithConstructorInModule.java
new file mode 100644
index 000000000..529b7f406
--- /dev/null
+++ b/tests/src/test/java/org/teavm/jso/test/ClassWithConstructorInModule.java
@@ -0,0 +1,36 @@
+/*
+ *  Copyright 2024 Alexey Andreev.
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+package org.teavm.jso.test;
+
+import org.teavm.jso.JSClass;
+import org.teavm.jso.JSModule;
+import org.teavm.jso.JSObject;
+import org.teavm.jso.JSProperty;
+
+@JSClass(name = "ClassWithConstructor")
+@JSModule("./testModule.js")
+public class ClassWithConstructorInModule implements JSObject {
+    public ClassWithConstructorInModule(int foo) {
+    }
+
+    public ClassWithConstructorInModule() {
+    }
+
+    @JSProperty
+    public native int getFoo();
+
+    public native String bar();
+}
diff --git a/tests/src/test/java/org/teavm/jso/test/ImportClassTest.java b/tests/src/test/java/org/teavm/jso/test/ImportClassTest.java
index 73ee8efc7..5c2adfdf5 100644
--- a/tests/src/test/java/org/teavm/jso/test/ImportClassTest.java
+++ b/tests/src/test/java/org/teavm/jso/test/ImportClassTest.java
@@ -21,6 +21,7 @@ import org.junit.runner.RunWith;
 import org.teavm.jso.JSBody;
 import org.teavm.jso.JSIndexer;
 import org.teavm.jso.JSObject;
+import org.teavm.junit.AttachJavaScript;
 import org.teavm.junit.EachTestCompiledSeparately;
 import org.teavm.junit.OnlyPlatform;
 import org.teavm.junit.SkipJVM;
@@ -41,6 +42,23 @@ public class ImportClassTest {
         assertEquals(42, o.get("bar"));
     }
 
+    @Test
+    @AttachJavaScript("org/teavm/jso/test/classWithConstructor.js")
+    public void constructor() {
+        var o = new ClassWithConstructor();
+        assertEquals(99, o.getFoo());
+        assertEquals("bar called", o.bar());
+
+        o = new ClassWithConstructor(23);
+        assertEquals(23, o.getFoo());
+    }
+
+    @Test
+    @AttachJavaScript("org/teavm/jso/test/classWithConstructor.js")
+    public void staticMethod() {
+        assertEquals("static method called", ClassWithConstructor.staticMethod());
+    }
+
     @JSBody(script = "return {};")
     private static native O create();
 
diff --git a/tests/src/test/java/org/teavm/jso/test/ImportModuleTest.java b/tests/src/test/java/org/teavm/jso/test/ImportModuleTest.java
index 46ddd2151..a8a47576c 100644
--- a/tests/src/test/java/org/teavm/jso/test/ImportModuleTest.java
+++ b/tests/src/test/java/org/teavm/jso/test/ImportModuleTest.java
@@ -56,6 +56,18 @@ public class ImportModuleTest {
         assertEquals(23, runTestFunction());
     }
 
+    @Test
+    @JsModuleTest
+    @ServeJS(from = "org/teavm/jso/test/classWithConstructorInModule.js", as = "testModule.js")
+    public void classConstructor() {
+        var o = new ClassWithConstructorInModule();
+        assertEquals(99, o.getFoo());
+        assertEquals("bar called", o.bar());
+
+        o = new ClassWithConstructorInModule(23);
+        assertEquals(23, o.getFoo());
+    }
+
     @JSBody(
             script = "return testModule.foo();",
             imports = @JSBodyImport(alias = "testModule", fromModule = "./testModule.js")
diff --git a/tests/src/test/resources/org/teavm/jso/test/classWithConstructor.js b/tests/src/test/resources/org/teavm/jso/test/classWithConstructor.js
new file mode 100644
index 000000000..4558db502
--- /dev/null
+++ b/tests/src/test/resources/org/teavm/jso/test/classWithConstructor.js
@@ -0,0 +1,33 @@
+/*
+ *  Copyright 2024 Alexey Andreev.
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+class ClassWithConstructor {
+    constructor(foo) {
+        this._foo = foo || 99;
+    }
+
+    get foo() {
+        return this._foo;
+    }
+
+    bar() {
+        return "bar called";
+    }
+
+    static staticMethod() {
+        return "static method called";
+    }
+}
\ No newline at end of file
diff --git a/tests/src/test/resources/org/teavm/jso/test/classWithConstructorInModule.js b/tests/src/test/resources/org/teavm/jso/test/classWithConstructorInModule.js
new file mode 100644
index 000000000..e88e51471
--- /dev/null
+++ b/tests/src/test/resources/org/teavm/jso/test/classWithConstructorInModule.js
@@ -0,0 +1,29 @@
+/*
+ *  Copyright 2024 Alexey Andreev.
+ *
+ *  Licensed under the Apache License, Version 2.0 (the "License");
+ *  you may not use this file except in compliance with the License.
+ *  You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ *  Unless required by applicable law or agreed to in writing, software
+ *  distributed under the License is distributed on an "AS IS" BASIS,
+ *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ *  See the License for the specific language governing permissions and
+ *  limitations under the License.
+ */
+
+export class ClassWithConstructor {
+    constructor(foo) {
+        this._foo = foo || 99;
+    }
+
+    get foo() {
+        return this._foo;
+    }
+
+    bar() {
+        return "bar called";
+    }
+}
\ No newline at end of file
diff --git a/tools/deobfuscator-js/src/main/java/org/teavm/tooling/deobfuscate/js/Deobfuscator.java b/tools/deobfuscator-js/src/main/java/org/teavm/tooling/deobfuscate/js/Deobfuscator.java
index 267765c8a..d4753fd9a 100644
--- a/tools/deobfuscator-js/src/main/java/org/teavm/tooling/deobfuscate/js/Deobfuscator.java
+++ b/tools/deobfuscator-js/src/main/java/org/teavm/tooling/deobfuscate/js/Deobfuscator.java
@@ -34,14 +34,14 @@ import org.teavm.jso.typedarrays.Int8Array;
 import org.teavm.model.MethodReference;
 
 public final class Deobfuscator {
-    private static final JSRegExp FRAME_PATTERN = JSRegExp.create(""
+    private static final JSRegExp FRAME_PATTERN = new JSRegExp(""
             + "(^ +at ([^(]+) *\\((.+):([0-9]+):([0-9]+)\\) *$)|"
             + "(^([^@]*)@(.+):([0-9]+):([0-9]+)$)");
     private DebugInformation debugInformation;
     private String classesFileName;
 
     public Deobfuscator(ArrayBuffer buffer, String classesFileName) throws IOException {
-        Int8Array array = Int8Array.create(buffer);
+        var array = new Int8Array(buffer);
         debugInformation = DebugInformation.read(new Int8ArrayInputStream(array));
         this.classesFileName = classesFileName;
     }
@@ -51,7 +51,7 @@ public final class Deobfuscator {
     }
 
     private static void loadDeobfuscator(String fileName, String classesFileName) {
-        XMLHttpRequest xhr = XMLHttpRequest.create();
+        var xhr = new XMLHttpRequest();
         xhr.setResponseType("arraybuffer");
         xhr.onComplete(() -> {
             installDeobfuscator(xhr.getResponse().cast(), classesFileName);