[gnome-shell/ewlsh/esm: 8/8] ESM Part 1




commit 91d6953ee6bc25aee662abeb2a6c4162245fef72
Author: Evan Welsh <contact evanwelsh com>
Date:   Fri Sep 17 23:48:04 2021 -0700

    ESM Part 1

 HACKING.md                                      |    4 +-
 js/gdm/authPrompt.js                            |   47 +-
 js/gdm/batch.js                                 |   14 +-
 js/gdm/credentialManager.js                     |    4 +-
 js/gdm/loginDialog.js                           |   51 +-
 js/gdm/oVirt.js                                 |   12 +-
 js/gdm/realmd.js                                |    8 +-
 js/gdm/util.js                                  |   63 +-
 js/gdm/vmware.js                                |   12 +-
 js/js-resources.gresource.xml                   |    3 +-
 js/misc/config.d.ts                             |   19 +
 js/misc/config.js.in                            |    2 +-
 js/misc/extensionUtils.js                       |   96 +-
 js/misc/fileUtilsModule.js                      |  117 ++
 js/misc/gnomeSession.js                         |   17 +-
 js/misc/history.js                              |   16 +-
 js/misc/ibusManager.js                          |   15 +-
 js/misc/inputMethod.js                          |   12 +-
 js/misc/introspect.js                           |   13 +-
 js/misc/jsParse.js                              |   13 +-
 js/misc/keyboardManager.js                      |   23 +-
 js/misc/loginManager.js                         |   20 +-
 js/misc/modemManager.js                         |   19 +-
 js/misc/objectManager.js                        |   10 +-
 js/misc/params.d.ts                             |    6 +
 js/misc/params.js                               |    2 +-
 js/misc/parentalControlsManager.js              |   17 +-
 js/misc/permissionStore.js                      |    6 +-
 js/misc/signals.d.ts                            |    9 +
 js/misc/signals.js                              |    3 +-
 js/misc/smartcardManager.js                     |   12 +-
 js/misc/systemActions.js                        |   21 +-
 js/misc/util.js                                 |   65 +-
 js/misc/weather.js                              |   19 +-
 js/perf/basic.js                                |   11 +-
 js/perf/core.js                                 |    8 +-
 js/perf/hwtest.js                               |   11 +-
 js/ui/accessDialog.js                           |   24 +-
 js/ui/altTab.js                                 |   80 +-
 js/ui/animation.js                              |   43 +-
 js/ui/appDisplay.js                             |  213 ++--
 js/ui/appFavorites.js                           |   12 +-
 js/ui/appMenu.js                                |   10 +-
 js/ui/audioDeviceSelection.js                   |   29 +-
 js/ui/background.js                             |   90 +-
 js/ui/backgroundMenu.js                         |   13 +-
 js/ui/barLevel.js                               |   11 +-
 js/ui/boxpointer.js                             |   28 +-
 js/ui/calendar.js                               |   66 +-
 js/ui/checkBox.js                               |    9 +-
 js/ui/closeDialog.js                            |   35 +-
 js/ui/{components/__init__.js => components.js} |   22 +-
 js/ui/components/automountManager.js            |   31 +-
 js/ui/components/autorunManager.js              |   58 +-
 js/ui/components/keyring.js                     |   31 +-
 js/ui/components/networkAgent.js                |   49 +-
 js/ui/components/polkitAgent.js                 |   41 +-
 js/ui/components/telepathyClient.js             |  220 ++--
 js/ui/ctrlAltTab.js                             |   20 +-
 js/ui/dash.js                                   |   97 +-
 js/ui/dateMenu.js                               |   73 +-
 js/ui/dialog.js                                 |   29 +-
 js/ui/dnd.js                                    |   95 +-
 js/ui/edgeDragAction.js                         |   17 +-
 js/ui/endSessionDialog.js                       |   60 +-
 js/ui/environment.js                            |  686 ++++++-----
 js/ui/extensionDownloader.js                    |   28 +-
 js/ui/extensionSystem.js                        |   69 +-
 js/ui/focusCaretTracker.js                      |    6 +-
 js/ui/grabHelper.js                             |   10 +-
 js/ui/ibusCandidatePopup.js                     |   27 +-
 js/ui/iconGrid.js                               |  175 ++-
 js/ui/inhibitShortcutsDialog.js                 |   27 +-
 js/ui/init.js                                   |   14 +-
 js/ui/kbdA11yDialog.js                          |   26 +-
 js/ui/keyboard.js                               |   81 +-
 js/ui/layout.js                                 |   91 +-
 js/ui/lightbox.js                               |   15 +-
 js/ui/locatePointer.js                          |    8 +-
 js/ui/lookingGlass.js                           |   86 +-
 js/ui/magnifier.js                              |  115 +-
 js/ui/main.js                                   | 1493 ++++++++++++-----------
 js/ui/messageList.js                            |   59 +-
 js/ui/messageTray.js                            |   94 +-
 js/ui/modalDialog.js                            |   43 +-
 js/ui/mpris.js                                  |   22 +-
 js/ui/notificationDaemon.js                     |   55 +-
 js/ui/osdMonitorLabeler.js                      |   13 +-
 js/ui/osdWindow.js                              |   24 +-
 js/ui/overview.js                               |   41 +-
 js/ui/overviewControls.js                       |   60 +-
 js/ui/padOsd.js                                 |   65 +-
 js/ui/pageIndicators.js                         |   10 +-
 js/ui/panel.js                                  |  140 ++-
 js/ui/panelMenu.js                              |   25 +-
 js/ui/pointerA11yTimeout.js                     |   15 +-
 js/ui/pointerWatcher.js                         |   10 +-
 js/ui/popupMenu.js                              |  119 +-
 js/ui/remoteSearch.js                           |   23 +-
 js/ui/ripples.js                                |    5 +-
 js/ui/runDialog.js                              |   26 +-
 js/ui/screenShield.js                           |   48 +-
 js/ui/screenshot.js                             |   45 +-
 js/ui/scripting.js                              |   56 +-
 js/ui/search.js                                 |   75 +-
 js/ui/searchController.js                       |   15 +-
 js/ui/sessionMode.js                            |   21 +-
 js/ui/shellDBus.js                              |   48 +-
 js/ui/shellEntry.js                             |   19 +-
 js/ui/shellMountOperation.js                    |   62 +-
 js/ui/slider.js                                 |   11 +-
 js/ui/status/accessibility.js                   |   18 +-
 js/ui/status/bluetooth.js                       |   42 +-
 js/ui/status/brightness.js                      |   14 +-
 js/ui/status/dwellClick.js                      |   10 +-
 js/ui/status/keyboard.js                        |  102 +-
 js/ui/status/location.js                        |   39 +-
 js/ui/status/network.js                         |  123 +-
 js/ui/status/nightLight.js                      |   13 +-
 js/ui/status/power.js                           |   16 +-
 js/ui/status/powerProfiles.js                   |   10 +-
 js/ui/status/remoteAccess.js                    |    9 +-
 js/ui/status/rfkill.js                          |   17 +-
 js/ui/status/system.js                          |   18 +-
 js/ui/status/thunderbolt.js                     |   30 +-
 js/ui/status/volume.js                          |   31 +-
 js/ui/swipeTracker.js                           |   38 +-
 js/ui/switchMonitor.js                          |   19 +-
 js/ui/switcherPopup.js                          |   36 +-
 js/ui/unlockDialog.js                           |   46 +-
 js/ui/userWidget.js                             |   33 +-
 js/ui/welcomeDialog.js                          |   19 +-
 js/ui/windowAttentionHandler.js                 |   29 +-
 js/ui/windowManager.js                          |  122 +-
 js/ui/windowMenu.js                             |   15 +-
 js/ui/windowPreview.js                          |   43 +-
 js/ui/workspace.js                              |   53 +-
 js/ui/workspaceAnimation.js                     |   17 +-
 js/ui/workspaceSwitcherPopup.js                 |   20 +-
 js/ui/workspaceThumbnail.js                     |   72 +-
 js/ui/workspacesView.js                         |   47 +-
 js/ui/xdndHandler.js                            |   13 +-
 src/main.c                                      |    4 +-
 tests/interactive/background-repeat.js          |    4 +-
 tests/interactive/background-size.js            |    6 +-
 tests/interactive/border-radius.js              |    4 +-
 tests/interactive/border-width.js               |    4 +-
 tests/interactive/borders.js                    |    3 +-
 tests/interactive/box-layout.js                 |    4 +-
 tests/interactive/box-shadow-animated.js        |    7 +-
 tests/interactive/box-shadows.js                |    3 +-
 tests/interactive/calendar.js                   |    4 +-
 tests/interactive/css-fonts.js                  |    4 +-
 tests/interactive/entry.js                      |    5 +-
 tests/interactive/gapplication.js               |   10 +-
 tests/interactive/icons.js                      |    4 +-
 tests/interactive/inline-style.js               |    4 +-
 tests/interactive/scroll-view-sizing.js         |   11 +-
 tests/interactive/scrolling.js                  |    5 +-
 tests/interactive/test-title.js                 |    5 +-
 tests/interactive/transitions.js                |    3 +-
 tests/testcommon/ui.js                          |   12 +-
 tests/unit/markup.js                            |    3 +-
 163 files changed, 4826 insertions(+), 2909 deletions(-)
---
diff --git a/HACKING.md b/HACKING.md
index bdcb9ba934..612b7fdba3 100644
--- a/HACKING.md
+++ b/HACKING.md
@@ -66,7 +66,7 @@ library. These headers are not installed, distributed or introspected.
 Use UpperCamelCase when importing modules to distinguish them from ordinary
 variables, e.g.
 ```javascript
-    const GLib = imports.gi.GLib;
+    import GLib from 'gi://GLib';
 ```
 Imports should be categorized into one of two places. The top-most import block
 should contain only "environment imports". These are either modules from
@@ -118,7 +118,7 @@ See [What's new in JavaScript 1.7](https://developer.mozilla.org/en/JavaScript/N
 There are many approaches to classes in JavaScript. We use standard ES6 classes
 whenever possible, that is when not inheriting from GObjects.
 ```javascript
-    var IconLabelMenuItem = class extends PopupMenu.PopupMenuBaseItem {
+    export class IconLabelMenuItem extends PopupMenu.PopupMenuBaseItem {
         constructor(icon, label) {
             super({ reactive: false });
             this.actor.add_child(icon);
diff --git a/js/gdm/authPrompt.js b/js/gdm/authPrompt.js
index d4edfb9ef5..b67d5b5906 100644
--- a/js/gdm/authPrompt.js
+++ b/js/gdm/authPrompt.js
@@ -1,29 +1,34 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported AuthPrompt */
 
-const { Clutter, GLib, GObject, Pango, Shell, St } = imports.gi;
+const { Clutter, GLib, GObject, Pango, Shell } = imports.gi;
 
-const Animation = imports.ui.animation;
-const Batch = imports.gdm.batch;
-const GdmUtil = imports.gdm.util;
-const OVirt = imports.gdm.oVirt;
-const Vmware = imports.gdm.vmware;
-const ShellEntry = imports.ui.shellEntry;
-const UserWidget = imports.ui.userWidget;
-const Util = imports.misc.util;
+import St from 'gi://St';
+import Gdm from 'gi://Gdm';
 
-var DEFAULT_BUTTON_WELL_ICON_SIZE = 16;
-var DEFAULT_BUTTON_WELL_ANIMATION_DELAY = 1000;
-var DEFAULT_BUTTON_WELL_ANIMATION_TIME = 300;
+import * as Animation from '../ui/animation.js';
+import * as Batch from './batch.js';
+import * as GdmUtil from './util.js';
+import * as OVirt from './oVirt.js';
+import * as Vmware from './vmware.js';
+import * as ShellEntry from '../ui/shellEntry.js';
+import * as UserWidget from '../ui/userWidget.js';
+import * as Util from '../misc/util.js';
 
-var MESSAGE_FADE_OUT_ANIMATION_TIME = 500;
+export let DEFAULT_BUTTON_WELL_ICON_SIZE = 16;
+export let DEFAULT_BUTTON_WELL_ANIMATION_DELAY = 1000;
+export let DEFAULT_BUTTON_WELL_ANIMATION_TIME = 300;
 
-var AuthPromptMode = {
+export let MESSAGE_FADE_OUT_ANIMATION_TIME = 500;
+
+/** @enum {number} */
+export const AuthPromptMode = {
     UNLOCK_ONLY: 0,
     UNLOCK_OR_LOG_IN: 1,
 };
 
-var AuthPromptStatus = {
+/** @enum {number} */
+export const AuthPromptStatus = {
     NOT_VERIFYING: 0,
     VERIFYING: 1,
     VERIFICATION_FAILED: 2,
@@ -32,13 +37,14 @@ var AuthPromptStatus = {
     VERIFICATION_IN_PROGRESS: 5,
 };
 
-var BeginRequestType = {
+/** @enum {number} */
+export const BeginRequestType = {
     PROVIDE_USERNAME: 0,
     DONT_PROVIDE_USERNAME: 1,
     REUSE_USERNAME: 2,
 };
 
-var AuthPrompt = GObject.registerClass({
+export const AuthPrompt = GObject.registerClass({
     Signals: {
         'cancelled': {},
         'failed': {},
@@ -47,6 +53,10 @@ var AuthPrompt = GObject.registerClass({
         'reset': { param_types: [GObject.TYPE_UINT] },
     },
 }, class AuthPrompt extends St.BoxLayout {
+    /**
+     * @param {Gdm.Client} gdmClient
+     * @param {AuthPromptMode} mode 
+     */
     _init(gdmClient, mode) {
         super._init({
             style_class: 'login-dialog-prompt-layout',
@@ -583,6 +593,9 @@ var AuthPrompt = GObject.registerClass({
         this._entry.clutter_text.insert_unichar(unichar);
     }
 
+    /**
+     * @param {{ userName?: string | null, hold?: Batch.Hold | null }} [params] 
+     */
     begin(params = {}) {
         let { userName = null, hold = null } = params;
 
diff --git a/js/gdm/batch.js b/js/gdm/batch.js
index f841f0f8e9..901e0d11a7 100644
--- a/js/gdm/batch.js
+++ b/js/gdm/batch.js
@@ -45,10 +45,10 @@
  */
 /* exported ConcurrentBatch, ConsecutiveBatch */
 
-const { GObject } = imports.gi;
-const Signals = imports.misc.signals;
+import GObject from 'gi://GObject';
+import * as Signals from '../misc/signals.js';
 
-var Task = class extends Signals.EventEmitter {
+export class Task extends Signals.EventEmitter {
     constructor(scope, handler) {
         super();
 
@@ -68,7 +68,7 @@ var Task = class extends Signals.EventEmitter {
     }
 };
 
-var Hold = class extends Task {
+export class Hold extends Task {
     constructor() {
         super(null, () => this);
 
@@ -104,7 +104,7 @@ var Hold = class extends Task {
     }
 };
 
-var Batch = class extends Task {
+export class Batch extends Task {
     constructor(scope, tasks) {
         super();
 
@@ -173,7 +173,7 @@ var Batch = class extends Task {
     }
 };
 
-var ConcurrentBatch = class extends Batch {
+export class ConcurrentBatch extends Batch {
     process() {
         let hold = this.runTask();
 
@@ -187,7 +187,7 @@ var ConcurrentBatch = class extends Batch {
     }
 };
 
-var ConsecutiveBatch = class extends Batch {
+export class ConsecutiveBatch extends Batch {
     process() {
         let hold = this.runTask();
 
diff --git a/js/gdm/credentialManager.js b/js/gdm/credentialManager.js
index c53de11df6..53bb2ded01 100644
--- a/js/gdm/credentialManager.js
+++ b/js/gdm/credentialManager.js
@@ -1,9 +1,9 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported CredentialManager */
 
-const Signals = imports.misc.signals;
+import * as Signals from "../misc/signals.js";
 
-var CredentialManager = class CredentialManager extends Signals.EventEmitter {
+export class CredentialManager extends Signals.EventEmitter {
     constructor(service) {
         super();
 
diff --git a/js/gdm/loginDialog.js b/js/gdm/loginDialog.js
index 760d7e9ef5..3eb8e41dbe 100644
--- a/js/gdm/loginDialog.js
+++ b/js/gdm/loginDialog.js
@@ -17,28 +17,40 @@
  * along with this program; if not, see <http://www.gnu.org/licenses/>.
  */
 
-const { AccountsService, Atk, Clutter, Gdm, Gio,
-        GLib, GObject, Meta, Pango, Shell, St } = imports.gi;
-
-const AuthPrompt = imports.gdm.authPrompt;
-const Batch = imports.gdm.batch;
-const BoxPointer = imports.ui.boxpointer;
-const CtrlAltTab = imports.ui.ctrlAltTab;
-const GdmUtil = imports.gdm.util;
-const Layout = imports.ui.layout;
-const LoginManager = imports.misc.loginManager;
-const Main = imports.ui.main;
-const PopupMenu = imports.ui.popupMenu;
-const Realmd = imports.gdm.realmd;
-const UserWidget = imports.ui.userWidget;
+import AccountsService from 'gi://AccountsService';
+import Atk from 'gi://Atk';
+import Clutter from 'gi://Clutter';
+import Gdm from 'gi://Gdm';
+import Gio from 'gi://Gio';
+import GLib from 'gi://GLib';
+import GObject from 'gi://GObject';
+import Meta from 'gi://Meta';
+import Pango from 'gi://Pango';
+import Shell from 'gi://Shell';
+import St from 'gi://St';
+
+import * as AuthPrompt from './authPrompt.js';
+import * as Batch from './batch.js';
+import * as BoxPointer from '../ui/boxpointer.js';
+import * as CtrlAltTab from '../ui/ctrlAltTab.js';
+import * as GdmUtil from './util.js';
+import * as Layout from '../ui/layout.js';
+import * as LoginManager from "../misc/loginManager.js";
+import Main from '../ui/main.js';
+import * as PopupMenu from '../ui/popupMenu.js';
+import * as Realmd from './realmd.js';
+import * as UserWidget from '../ui/userWidget.js';
 
 const _FADE_ANIMATION_TIME = 250;
 const _SCROLL_ANIMATION_TIME = 500;
 const _TIMED_LOGIN_IDLE_THRESHOLD = 5.0;
 
-var UserListItem = GObject.registerClass({
+export const UserListItem = GObject.registerClass({
     Signals: { 'activate': {} },
 }, class UserListItem extends St.Button {
+    /**
+     * @param {AccountsService.User} user 
+     */
     _init(user) {
         let layout = new St.BoxLayout({
             vertical: true,
@@ -152,7 +164,7 @@ var UserListItem = GObject.registerClass({
     }
 });
 
-var UserList = GObject.registerClass({
+export const UserList = GObject.registerClass({
     Signals: {
         'activate': { param_types: [UserListItem.$gtype] },
         'item-added': { param_types: [UserListItem.$gtype] },
@@ -304,7 +316,7 @@ var UserList = GObject.registerClass({
     }
 });
 
-var SessionMenuButton = GObject.registerClass({
+export const SessionMenuButton = GObject.registerClass({
     Signals: { 'session-activated': { param_types: [GObject.TYPE_STRING] } },
 }, class SessionMenuButton extends St.Bin {
     _init() {
@@ -400,12 +412,15 @@ var SessionMenuButton = GObject.registerClass({
     }
 });
 
-var LoginDialog = GObject.registerClass({
+export const LoginDialog = GObject.registerClass({
     Signals: {
         'failed': {},
         'wake-up-screen': {},
     },
 }, class LoginDialog extends St.Widget {
+    /**
+     * @param {Clutter.Actor} parentActor
+     */
     _init(parentActor) {
         super._init({ style_class: 'login-dialog', visible: false });
 
diff --git a/js/gdm/oVirt.js b/js/gdm/oVirt.js
index 4cf17d99e2..6e9a5211f8 100644
--- a/js/gdm/oVirt.js
+++ b/js/gdm/oVirt.js
@@ -1,10 +1,9 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
-/* exported getOVirtCredentialsManager */
 
-const Gio = imports.gi.Gio;
-const Credential = imports.gdm.credentialManager;
+import Gio from 'gi://Gio';
+import * as Credential from './credentialManager.js';
 
-var SERVICE_NAME = 'gdm-ovirtcred';
+export const SERVICE_NAME = 'gdm-ovirtcred';
 
 const OVirtCredentialsIface = `
 <node>
@@ -17,6 +16,7 @@ const OVirtCredentialsIface = `
 
 const OVirtCredentialsInfo = Gio.DBusInterfaceInfo.new_for_xml(OVirtCredentialsIface);
 
+/** @type {OVirtCredentialsManager | null} */
 let _oVirtCredentialsManager = null;
 
 function OVirtCredentials() {
@@ -30,7 +30,7 @@ function OVirtCredentials() {
     return self;
 }
 
-var OVirtCredentialsManager = class OVirtCredentialsManager extends Credential.CredentialManager {
+export class OVirtCredentialsManager extends Credential.CredentialManager {
     constructor() {
         super(SERVICE_NAME);
         this._credentials = OVirtCredentials();
@@ -41,7 +41,7 @@ var OVirtCredentialsManager = class OVirtCredentialsManager extends Credential.C
     }
 };
 
-function getOVirtCredentialsManager() {
+export function getOVirtCredentialsManager() {
     if (!_oVirtCredentialsManager)
         _oVirtCredentialsManager = new OVirtCredentialsManager();
 
diff --git a/js/gdm/realmd.js b/js/gdm/realmd.js
index b7ff5ad547..44335b3dcf 100644
--- a/js/gdm/realmd.js
+++ b/js/gdm/realmd.js
@@ -1,10 +1,10 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported Manager */
 
-const Gio = imports.gi.Gio;
-const Signals = imports.misc.signals;
+import Gio from 'gi://Gio';
+import * as Signals from '../misc/signals.js';
 
-const { loadInterfaceXML } = imports.misc.fileUtils;
+import { loadInterfaceXML } from '../misc/fileUtilsModule.js';
 
 const ProviderIface = loadInterfaceXML("org.freedesktop.realmd.Provider");
 const Provider = Gio.DBusProxy.makeProxyWrapper(ProviderIface);
@@ -15,7 +15,7 @@ const Service = Gio.DBusProxy.makeProxyWrapper(ServiceIface);
 const RealmIface = loadInterfaceXML("org.freedesktop.realmd.Realm");
 const Realm = Gio.DBusProxy.makeProxyWrapper(RealmIface);
 
-var Manager = class extends Signals.EventEmitter {
+export class Manager extends Signals.EventEmitter {
     constructor() {
         super();
 
diff --git a/js/gdm/util.js b/js/gdm/util.js
index 0adb22adde..7f4f2715c6 100644
--- a/js/gdm/util.js
+++ b/js/gdm/util.js
@@ -3,15 +3,18 @@
             DISABLE_USER_LIST_KEY, fadeInActor, fadeOutActor, cloneAndFadeOutActor,
             ShellUserVerifier */
 
-const { Clutter, Gdm, Gio, GLib } = imports.gi;
-const Signals = imports.misc.signals;
-
-const Batch = imports.gdm.batch;
-const OVirt = imports.gdm.oVirt;
-const Vmware = imports.gdm.vmware;
-const Main = imports.ui.main;
-const { loadInterfaceXML } = imports.misc.fileUtils;
-const SmartcardManager = imports.misc.smartcardManager;
+import Clutter from 'gi://Clutter';
+import Gdm from 'gi://Gdm';
+import Gio from 'gi://Gio';
+import GLib from 'gi://GLib';
+import * as Signals from '../misc/signals.js';
+
+import * as Batch from './batch.js';
+import * as OVirt from './oVirt.js';
+import * as Vmware from './vmware.js';
+import Main from '../ui/main.js';
+import { loadInterfaceXML } from '../misc/fileUtilsModule.js';
+import * as SmartcardManager from '../misc/smartcardManager.js';
 
 const FprintManagerIface = loadInterfaceXML('net.reactivated.Fprint.Manager');
 const FprintManagerProxy = Gio.DBusProxy.makeProxyWrapper(FprintManagerIface);
@@ -27,29 +30,29 @@ Gio._promisify(Gdm.UserVerifierProxy.prototype,
 Gio._promisify(Gdm.UserVerifierProxy.prototype,
     'call_begin_verification', 'call_begin_verification_finish');
 
-var PASSWORD_SERVICE_NAME = 'gdm-password';
-var FINGERPRINT_SERVICE_NAME = 'gdm-fingerprint';
-var SMARTCARD_SERVICE_NAME = 'gdm-smartcard';
-var FADE_ANIMATION_TIME = 160;
-var CLONE_FADE_ANIMATION_TIME = 250;
+export const PASSWORD_SERVICE_NAME = 'gdm-password';
+export const FINGERPRINT_SERVICE_NAME = 'gdm-fingerprint';
+export const SMARTCARD_SERVICE_NAME = 'gdm-smartcard';
+export const FADE_ANIMATION_TIME = 160;
+export const CLONE_FADE_ANIMATION_TIME = 250;
 
-var LOGIN_SCREEN_SCHEMA = 'org.gnome.login-screen';
-var PASSWORD_AUTHENTICATION_KEY = 'enable-password-authentication';
-var FINGERPRINT_AUTHENTICATION_KEY = 'enable-fingerprint-authentication';
-var SMARTCARD_AUTHENTICATION_KEY = 'enable-smartcard-authentication';
-var BANNER_MESSAGE_KEY = 'banner-message-enable';
-var BANNER_MESSAGE_TEXT_KEY = 'banner-message-text';
-var ALLOWED_FAILURES_KEY = 'allowed-failures';
+export const LOGIN_SCREEN_SCHEMA = 'org.gnome.login-screen';
+export const PASSWORD_AUTHENTICATION_KEY = 'enable-password-authentication';
+export const FINGERPRINT_AUTHENTICATION_KEY = 'enable-fingerprint-authentication';
+export const SMARTCARD_AUTHENTICATION_KEY = 'enable-smartcard-authentication';
+export const BANNER_MESSAGE_KEY = 'banner-message-enable';
+export const BANNER_MESSAGE_TEXT_KEY = 'banner-message-text';
+export const ALLOWED_FAILURES_KEY = 'allowed-failures';
 
-var LOGO_KEY = 'logo';
-var DISABLE_USER_LIST_KEY = 'disable-user-list';
+export const LOGO_KEY = 'logo';
+export const DISABLE_USER_LIST_KEY = 'disable-user-list';
 
 // Give user 48ms to read each character of a PAM message
-var USER_READ_TIME = 48;
+export const USER_READ_TIME = 48;
 const FINGERPRINT_ERROR_TIMEOUT_WAIT = 15;
 
-var MessageType = {
-    // Keep messages in order by priority
+/** @enum {number} */
+export const MessageType = {
     NONE: 0,
     HINT: 1,
     INFO: 2,
@@ -62,7 +65,7 @@ const FingerprintReaderType = {
     SWIPE: 2,
 };
 
-function fadeInActor(actor) {
+export function fadeInActor(actor) {
     if (actor.opacity == 255 && actor.visible)
         return null;
 
@@ -86,7 +89,7 @@ function fadeInActor(actor) {
     return hold;
 }
 
-function fadeOutActor(actor) {
+export function fadeOutActor(actor) {
     if (!actor.visible || actor.opacity == 0) {
         actor.opacity = 0;
         actor.hide();
@@ -108,7 +111,7 @@ function fadeOutActor(actor) {
     return hold;
 }
 
-function cloneAndFadeOutActor(actor) {
+export function cloneAndFadeOutActor(actor) {
     // Immediately hide actor so its sibling can have its space
     // and position, but leave a non-reactive clone on-screen,
     // so from the user's point of view it smoothly fades away
@@ -136,7 +139,7 @@ function cloneAndFadeOutActor(actor) {
     return hold;
 }
 
-var ShellUserVerifier = class extends Signals.EventEmitter {
+export class ShellUserVerifier extends Signals.EventEmitter {
     constructor(client, params = {}) {
         super();
 
diff --git a/js/gdm/vmware.js b/js/gdm/vmware.js
index 118b8af391..6373d23a42 100644
--- a/js/gdm/vmware.js
+++ b/js/gdm/vmware.js
@@ -1,13 +1,12 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
-/* exported getVmwareCredentialsManager */
 
-const Gio = imports.gi.Gio;
-const Credential = imports.gdm.credentialManager;
+import Gio from 'gi://Gio';
+import * as Credential from './credentialManager.js';
 
 const dbusPath = '/org/vmware/viewagent/Credentials';
 const dbusInterface = 'org.vmware.viewagent.Credentials';
 
-var SERVICE_NAME = 'gdm-vmwcred';
+export const SERVICE_NAME = 'gdm-vmwcred';
 
 const VmwareCredentialsIface = '<node> \
 <interface name="' + dbusInterface + '"> \
@@ -20,6 +19,7 @@ const VmwareCredentialsIface = '<node> \
 
 const VmwareCredentialsInfo = Gio.DBusInterfaceInfo.new_for_xml(VmwareCredentialsIface);
 
+/** @type {VmwareCredentialsManager | null} */
 let _vmwareCredentialsManager = null;
 
 function VmwareCredentials() {
@@ -33,7 +33,7 @@ function VmwareCredentials() {
     return self;
 }
 
-var VmwareCredentialsManager = class VmwareCredentialsManager extends Credential.CredentialManager {
+export class VmwareCredentialsManager extends Credential.CredentialManager {
     constructor() {
         super(SERVICE_NAME);
         this._credentials = VmwareCredentials();
@@ -44,7 +44,7 @@ var VmwareCredentialsManager = class VmwareCredentialsManager extends Credential
     }
 };
 
-function getVmwareCredentialsManager() {
+export function getVmwareCredentialsManager() {
     if (!_vmwareCredentialsManager)
         _vmwareCredentialsManager = new VmwareCredentialsManager();
 
diff --git a/js/js-resources.gresource.xml b/js/js-resources.gresource.xml
index 32d41e11ec..9861f183fd 100644
--- a/js/js-resources.gresource.xml
+++ b/js/js-resources.gresource.xml
@@ -13,6 +13,7 @@
     <file>misc/config.js</file>
     <file>misc/extensionUtils.js</file>
     <file>misc/fileUtils.js</file>
+    <file>misc/fileUtilsModule.js</file>
     <file>misc/gnomeSession.js</file>
     <file>misc/history.js</file>
     <file>misc/ibusManager.js</file>
@@ -120,7 +121,7 @@
     <file>ui/workspacesView.js</file>
     <file>ui/xdndHandler.js</file>
 
-    <file>ui/components/__init__.js</file>
+    <file>ui/components.js</file>
     <file>ui/components/autorunManager.js</file>
     <file>ui/components/automountManager.js</file>
     <file>ui/components/networkAgent.js</file>
diff --git a/js/misc/config.d.ts b/js/misc/config.d.ts
new file mode 100644
index 0000000000..1f1139e26b
--- /dev/null
+++ b/js/misc/config.d.ts
@@ -0,0 +1,19 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+
+/* The name of this package (not localized) */
+export const PACKAGE_NAME: string;
+/* The version of this package */
+export const PACKAGE_VERSION: string;
+/* 1 if gnome-bluetooth is available, 0 otherwise */
+export const HAVE_BLUETOOTH: boolean;
+/* 1 if networkmanager is available, 0 otherwise */
+export const HAVE_NETWORKMANAGER: boolean;
+/* gettext package */
+export const GETTEXT_PACKAGE: string;
+/* locale dir */
+export const LOCALEDIR: string;
+/* other standard directories */
+export const LIBEXECDIR: string;
+export const PKGDATADIR: string;
+/* g-i package versions */
+export const LIBMUTTER_API_VERSION: string;
diff --git a/js/misc/config.js.in b/js/misc/config.js.in
index f9210397a1..93d9052498 100644
--- a/js/misc/config.js.in
+++ b/js/misc/config.js.in
@@ -18,4 +18,4 @@ var LOCALEDIR = '@datadir@/locale';
 var LIBEXECDIR = '@libexecdir@';
 var PKGDATADIR = '@datadir@/@PACKAGE_NAME@';
 /* g-i package versions */
-var LIBMUTTER_API_VERSION = '@LIBMUTTER_API_VERSION@'
+var LIBMUTTER_API_VERSION = '@LIBMUTTER_API_VERSION@';
diff --git a/js/misc/extensionUtils.js b/js/misc/extensionUtils.js
index c86468e4b4..67c7974999 100644
--- a/js/misc/extensionUtils.js
+++ b/js/misc/extensionUtils.js
@@ -7,18 +7,19 @@
 // Common utils for the extension system and the extension
 // preferences tool
 
-const { Gio, GLib } = imports.gi;
+import Gio from 'gi://Gio';
+import GLib from 'gi://GLib';
 
-const Gettext = imports.gettext;
+import * as Gettext from 'gettext';
 
 const Config = imports.misc.config;
 
-var ExtensionType = {
+export const ExtensionType = {
     SYSTEM: 1,
     PER_USER: 2,
 };
 
-var ExtensionState = {
+export const ExtensionState = {
     ENABLED: 1,
     DISABLED: 2,
     ERROR: 3,
@@ -41,13 +42,36 @@ const SERIALIZED_PROPERTIES = [
     'canChange',
 ];
 
+let extension;
+/** @type {typeof import('../ui/main.js').default} */
+let Main;
+
+export function _setCurrentExtension(currentExtension) {
+    extension = currentExtension;
+}
+
+/** @param {typeof import('../ui/main.js').default} main */
+export function _setMain(main) {
+    Main = main;
+}
+
+/** @typedef {object} Extension */
+
 /**
  * getCurrentExtension:
  *
- * @returns {?object} - The current extension, or null if not called from
+ * @returns {Extension} - The current extension, or null if not called from
  * an extension.
  */
-function getCurrentExtension() {
+export function getCurrentExtension() {
+    if (extension) {
+        return extension;
+    }
+
+    if (!Main) {
+        throw new Error(`getCurrentExtension cannot be called before ExtensionUtils._setMain`);
+    }
+
     let stack = new Error().stack.split('\n');
     let extensionStackLine;
 
@@ -74,7 +98,6 @@ function getCurrentExtension() {
 
     // local import, as the module is used from outside the gnome-shell process
     // as well (not this function though)
-    let extensionManager = imports.ui.main.extensionManager;
 
     let path = match[1];
     let file = Gio.File.new_for_path(path);
@@ -82,7 +105,7 @@ function getCurrentExtension() {
     // Walk up the directory tree, looking for an extension with
     // the same UUID as a directory name.
     while (file != null) {
-        let extension = extensionManager.lookup(file.get_basename());
+        let extension = Main.extensionManager.lookup(file.get_basename());
         if (extension !== undefined)
             return extension;
         file = file.get_parent();
@@ -98,7 +121,7 @@ function getCurrentExtension() {
  * Initialize Gettext to load translations from extensionsdir/locale.
  * If @domain is not provided, it will be taken from metadata['gettext-domain']
  */
-function initTranslations(domain) {
+export function initTranslations(domain) {
     let extension = getCurrentExtension();
 
     if (!extension)
@@ -176,13 +199,13 @@ function callExtensionGettextFunc(func, ...args) {
 /**
  * getSettings:
  * @param {string=} schema - the GSettings schema id
- * @returns {Gio.Settings} - a new settings object for @schema
+ * @returns {import("gi://Gio").Settings} - a new settings object for @schema
  *
  * Builds and returns a GSettings schema for @schema, using schema files
  * in extensionsdir/schemas. If @schema is omitted, it is taken from
  * metadata['settings-schema'].
  */
-function getSettings(schema) {
+export function getSettings(schema) {
     let extension = getCurrentExtension();
 
     if (!extension)
@@ -198,8 +221,8 @@ function getSettings(schema) {
     let schemaSource;
     if (schemaDir.query_exists(null)) {
         schemaSource = GioSSS.new_from_directory(schemaDir.get_path(),
-                                                 GioSSS.get_default(),
-                                                 false);
+            GioSSS.get_default(),
+            false);
     } else {
         schemaSource = GioSSS.get_default();
     }
@@ -216,15 +239,14 @@ function getSettings(schema) {
  *
  * Open the preference dialog of the current extension
  */
-function openPrefs() {
+export function openPrefs() {
     const extension = getCurrentExtension();
 
     if (!extension)
         throw new Error('openPrefs() can only be called from extensions');
 
     try {
-        const extensionManager = imports.ui.main.extensionManager;
-        extensionManager.openExtensionPrefs(extension.uuid, '', {});
+        Main.extensionManager.openExtensionPrefs(extension.uuid, '', {});
     } catch (e) {
         if (e.name === 'ImportError')
             throw new Error('openPrefs() cannot be called from preferences');
@@ -232,34 +254,37 @@ function openPrefs() {
     }
 }
 
-function isOutOfDate(extension) {
+export function isOutOfDate(extension) {
     const [major] = Config.PACKAGE_VERSION.split('.');
     return !extension.metadata['shell-version'].some(v => v.startsWith(major));
 }
 
-function serializeExtension(extension) {
-    let obj = { ...extension.metadata };
+export function serializeExtension(extension) {
+    let obj = {};
+    Object.assign(obj, extension.metadata);
 
     SERIALIZED_PROPERTIES.forEach(prop => {
         obj[prop] = extension[prop];
     });
 
+    /** @type {{ [key: string]: GLib.Variant<'b' | 's' | 'd'>}} */
     let res = {};
     for (let key in obj) {
         let val = obj[key];
+        /** @type {'s' | 'd' | 'b'} */
         let type;
         switch (typeof val) {
-        case 'string':
-            type = 's';
-            break;
-        case 'number':
-            type = 'd';
-            break;
-        case 'boolean':
-            type = 'b';
-            break;
-        default:
-            continue;
+            case 'string':
+                type = 's';
+                break;
+            case 'number':
+                type = 'd';
+                break;
+            case 'boolean':
+                type = 'b';
+                break;
+            default:
+                continue;
         }
         res[key] = GLib.Variant.new(type, val);
     }
@@ -267,7 +292,7 @@ function serializeExtension(extension) {
     return res;
 }
 
-function deserializeExtension(variant) {
+export function deserializeExtension(variant) {
     let res = { metadata: {} };
     for (let prop in variant) {
         let val = variant[prop].unpack();
@@ -282,11 +307,4 @@ function deserializeExtension(variant) {
     return res;
 }
 
-function installImporter(extension) {
-    let oldSearchPath = imports.searchPath.slice();  // make a copy
-    imports.searchPath = [extension.dir.get_parent().get_path()];
-    // importing a "subdir" creates a new importer object that doesn't affect
-    // the global one
-    extension.imports = imports[extension.uuid];
-    imports.searchPath = oldSearchPath;
-}
+// extension.dir.get_parent().get_path()
diff --git a/js/misc/fileUtilsModule.js b/js/misc/fileUtilsModule.js
new file mode 100644
index 0000000000..b452d36794
--- /dev/null
+++ b/js/misc/fileUtilsModule.js
@@ -0,0 +1,117 @@
+// -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
+
+import Gio from 'gi://Gio';
+import GLib from 'gi://GLib';
+
+const Config = imports.misc.config;
+
+export function collectFromDatadirs(subdir, includeUserDir, processFile) {
+    let dataDirs = GLib.get_system_data_dirs();
+    if (includeUserDir)
+        dataDirs.unshift(GLib.get_user_data_dir());
+
+    for (let i = 0; i < dataDirs.length; i++) {
+        let path = GLib.build_filenamev([dataDirs[i], 'gnome-shell', subdir]);
+        let dir = Gio.File.new_for_path(path);
+
+        let fileEnum;
+        try {
+            fileEnum = dir.enumerate_children('standard::name,standard::type',
+                                              Gio.FileQueryInfoFlags.NONE, null);
+        } catch (e) {
+            fileEnum = null;
+        }
+        if (fileEnum != null) {
+            let info;
+            while ((info = fileEnum.next_file(null)))
+                processFile(fileEnum.get_child(info), info);
+        }
+    }
+}
+
+export function recursivelyDeleteDir(dir, deleteParent) {
+    let children = dir.enumerate_children('standard::name,standard::type',
+                                          Gio.FileQueryInfoFlags.NONE, null);
+
+    let info;
+    while ((info = children.next_file(null)) != null) {
+        let type = info.get_file_type();
+        let child = dir.get_child(info.get_name());
+        if (type == Gio.FileType.REGULAR)
+            child.delete(null);
+        else if (type == Gio.FileType.DIRECTORY)
+            recursivelyDeleteDir(child, true);
+    }
+
+    if (deleteParent)
+        dir.delete(null);
+}
+
+export function recursivelyMoveDir(srcDir, destDir) {
+    let children = srcDir.enumerate_children('standard::name,standard::type',
+                                             Gio.FileQueryInfoFlags.NONE, null);
+
+    if (!destDir.query_exists(null))
+        destDir.make_directory_with_parents(null);
+
+    let info;
+    while ((info = children.next_file(null)) != null) {
+        let type = info.get_file_type();
+        let srcChild = srcDir.get_child(info.get_name());
+        let destChild = destDir.get_child(info.get_name());
+        if (type == Gio.FileType.REGULAR)
+            srcChild.move(destChild, Gio.FileCopyFlags.NONE, null, null);
+        else if (type == Gio.FileType.DIRECTORY)
+            recursivelyMoveDir(srcChild, destChild);
+    }
+}
+
+let _ifaceResource = null;
+function ensureIfaceResource() {
+    if (_ifaceResource)
+        return;
+
+    // don't use global.datadir so the method is usable from tests/tools
+    let dir = GLib.getenv('GNOME_SHELL_DATADIR') || Config.PKGDATADIR;
+    let path = `${dir}/gnome-shell-dbus-interfaces.gresource`;
+    _ifaceResource = Gio.Resource.load(path);
+    _ifaceResource._register();
+}
+
+export function loadInterfaceXML(iface) {
+    ensureIfaceResource();
+
+    let uri = `resource:///org/gnome/shell/dbus-interfaces/${iface}.xml`;
+    let f = Gio.File.new_for_uri(uri);
+
+    try {
+        let [ok_, bytes] = f.load_contents(null);
+        return imports.byteArray.toString(bytes);
+    } catch (e) {
+        log(`Failed to load D-Bus interface ${iface}`);
+    }
+
+    return null;
+}
+
+export function loadSubInterfaceXML(iface, ifaceFile) {
+    let xml = loadInterfaceXML(ifaceFile);
+    if (!xml)
+        return null;
+
+    let ifaceStartTag = `<interface name="${iface}">`;
+    let ifaceStopTag = '</interface>';
+    let ifaceStartIndex = xml.indexOf(ifaceStartTag);
+    let ifaceEndIndex = xml.indexOf(ifaceStopTag, ifaceStartIndex + 1) + ifaceStopTag.length;
+
+    let xmlHeader = '<!DOCTYPE node PUBLIC\n' +
+        '\'-//freedesktop//DTD D-BUS Object Introspection 1.0//EN\'\n' +
+        '\'http://www.freedesktop.org/standards/dbus/1.0/introspect.dtd\'>\n' +
+        '<node>\n';
+    let xmlFooter = '</node>';
+
+    return (
+        xmlHeader +
+        xml.substr(ifaceStartIndex, ifaceEndIndex - ifaceStartIndex) +
+        xmlFooter);
+}
diff --git a/js/misc/gnomeSession.js b/js/misc/gnomeSession.js
index a77930975c..bcfc424594 100644
--- a/js/misc/gnomeSession.js
+++ b/js/misc/gnomeSession.js
@@ -1,21 +1,22 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported PresenceStatus, Presence, Inhibitor, SessionManager, InhibitFlags */
 
-const Gio = imports.gi.Gio;
+import Gio from 'gi://Gio';
 
-const { loadInterfaceXML } = imports.misc.fileUtils;
+import { loadInterfaceXML } from "../misc/fileUtilsModule.js";
 
 const PresenceIface = loadInterfaceXML('org.gnome.SessionManager.Presence');
 
-var PresenceStatus = {
+/** @enum {number} */
+export const PresenceStatus = {
     AVAILABLE: 0,
     INVISIBLE: 1,
     BUSY: 2,
     IDLE: 3,
 };
 
-var PresenceProxy = Gio.DBusProxy.makeProxyWrapper(PresenceIface);
-function Presence(initCallback, cancellable) {
+export const PresenceProxy = Gio.DBusProxy.makeProxyWrapper(PresenceIface);
+export function Presence(initCallback, cancellable) {
     return new PresenceProxy(Gio.DBus.session, 'org.gnome.SessionManager',
                          '/org/gnome/SessionManager/Presence', initCallback, cancellable);
 }
@@ -25,18 +26,18 @@ function Presence(initCallback, cancellable) {
 // of new inhibitors)
 const InhibitorIface = loadInterfaceXML('org.gnome.SessionManager.Inhibitor');
 var InhibitorProxy = Gio.DBusProxy.makeProxyWrapper(InhibitorIface);
-function Inhibitor(objectPath, initCallback, cancellable) {
+export function Inhibitor(objectPath, initCallback, cancellable) {
     return InhibitorProxy(Gio.DBus.session, 'org.gnome.SessionManager', objectPath, initCallback, 
cancellable);
 }
 
 // Not the full interface, only the methods we use
 const SessionManagerIface = loadInterfaceXML('org.gnome.SessionManager');
 var SessionManagerProxy = Gio.DBusProxy.makeProxyWrapper(SessionManagerIface);
-function SessionManager(initCallback, cancellable) {
+export function SessionManager(initCallback, cancellable) {
     return SessionManagerProxy(Gio.DBus.session, 'org.gnome.SessionManager', '/org/gnome/SessionManager', 
initCallback, cancellable);
 }
 
-var InhibitFlags = {
+export const InhibitFlags = {
     LOGOUT: 1 << 0,
     SWITCH: 1 << 1,
     SUSPEND: 1 << 2,
diff --git a/js/misc/history.js b/js/misc/history.js
index b8b66b7e0f..30b694d7c6 100644
--- a/js/misc/history.js
+++ b/js/misc/history.js
@@ -1,12 +1,22 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported HistoryManager */
 
-const Signals = imports.misc.signals;
-const Clutter = imports.gi.Clutter;
+import * as Signals from './signals.js';
+import Clutter from 'gi://Clutter';
 
 var DEFAULT_LIMIT = 512;
 
-var HistoryManager = class extends Signals.EventEmitter {
+/** 
+ * @typedef {object} HistoryManagerParams
+ * @property {string | null} gsettingsKey
+ * @property {number} limit
+ * @property {Clutter.Text} entry
+ */
+
+ export class HistoryManager extends Signals.EventEmitter {
+    /**
+     * @param {Partial<HistoryManagerParams>} params 
+     */
     constructor(params = {}) {
         super();
 
diff --git a/js/misc/ibusManager.js b/js/misc/ibusManager.js
index 056bf543f2..9e9f707b6d 100644
--- a/js/misc/ibusManager.js
+++ b/js/misc/ibusManager.js
@@ -1,10 +1,12 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
-/* exported getIBusManager */
 
-const { Gio, GLib, IBus, Meta } = imports.gi;
-const Signals = imports.misc.signals;
+import Gio from 'gi://Gio';
+import GLib from 'gi://GLib';
+import IBus from 'gi://IBus';
+import Meta from 'gi://Meta';
+import * as Signals from './signals.js';
 
-const IBusCandidatePopup = imports.ui.ibusCandidatePopup;
+import * as IBusCandidatePopup from '../ui/ibusCandidatePopup.js';
 
 Gio._promisify(IBus.Bus.prototype,
     'list_engines_async', 'list_engines_async_finish');
@@ -18,6 +20,7 @@ Gio._promisify(IBus.Bus.prototype,
 // Ensure runtime version matches
 _checkIBusVersion(1, 5, 2);
 
+/** @type {IBusManager | null} */
 let _ibusManager = null;
 
 function _checkIBusVersion(requiredMajor, requiredMinor, requiredMicro) {
@@ -32,13 +35,13 @@ function _checkIBusVersion(requiredMajor, requiredMinor, requiredMicro) {
                 requiredMajor, requiredMinor, requiredMicro);
 }
 
-function getIBusManager() {
+export function getIBusManager() {
     if (_ibusManager == null)
         _ibusManager = new IBusManager();
     return _ibusManager;
 }
 
-var IBusManager = class extends Signals.EventEmitter {
+export class IBusManager extends Signals.EventEmitter {
     constructor() {
         super();
 
diff --git a/js/misc/inputMethod.js b/js/misc/inputMethod.js
index 25b02e35b8..67e1530892 100644
--- a/js/misc/inputMethod.js
+++ b/js/misc/inputMethod.js
@@ -1,15 +1,19 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported InputMethod */
-const { Clutter, GLib, Gio, GObject, IBus } = imports.gi;
+import Clutter from 'gi://Clutter';
+import GLib from 'gi://GLib';
+import Gio from 'gi://Gio';
+import GObject from 'gi://GObject';
+import IBus from 'gi://IBus';
 
-const Keyboard = imports.ui.status.keyboard;
+import * as Keyboard from '../ui/status/keyboard.js';
 
 Gio._promisify(IBus.Bus.prototype,
     'create_input_context_async', 'create_input_context_async_finish');
 
-var HIDE_PANEL_TIME = 50;
+export let HIDE_PANEL_TIME = 50;
 
-var InputMethod = GObject.registerClass(
+export const InputMethod = GObject.registerClass(
 class InputMethod extends Clutter.InputMethod {
     _init() {
         super._init();
diff --git a/js/misc/introspect.js b/js/misc/introspect.js
index 22bd8319c4..2a0c7d38a3 100644
--- a/js/misc/introspect.js
+++ b/js/misc/introspect.js
@@ -1,5 +1,9 @@
 /* exported IntrospectService */
-const { Gio, GLib, Meta, Shell, St } = imports.gi;
+import Gio from 'gi://Gio';
+import GLib from 'gi://GLib';
+import Meta from 'gi://Meta';
+import Shell from 'gi://Shell';
+import St from 'gi://St';
 
 const APP_ALLOWLIST = [
     'org.freedesktop.impl.portal.desktop.gtk',
@@ -8,12 +12,12 @@ const APP_ALLOWLIST = [
 
 const INTROSPECT_DBUS_API_VERSION = 3;
 
-const { loadInterfaceXML } = imports.misc.fileUtils;
-const { DBusSenderChecker } = imports.misc.util;
+import { loadInterfaceXML } from "./fileUtilsModule.js";
+import { DBusSenderChecker } from "./util.js";
 
 const IntrospectDBusIface = loadInterfaceXML('org.gnome.Shell.Introspect');
 
-var IntrospectService = class {
+export class IntrospectService {
     constructor() {
         this._dbusImpl = Gio.DBusExportedObject.wrapJSObject(IntrospectDBusIface,
                                                              this);
@@ -131,6 +135,7 @@ var IntrospectService = class {
     GetWindowsAsync(params, invocation) {
         let focusWindow = global.display.get_focus_window();
         let apps = this._appSystem.get_running();
+        /** @type {{ [key: number]: { [key: string]: GLib.Variant }}} */
         let windowsList = {};
 
         try {
diff --git a/js/misc/jsParse.js b/js/misc/jsParse.js
index c4e077f626..293dd6f5a0 100644
--- a/js/misc/jsParse.js
+++ b/js/misc/jsParse.js
@@ -1,5 +1,4 @@
 /* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
-/* exported getCompletions, getCommonPrefix, getDeclaredConstants */
 
 // Returns a list of potential completions for text. Completions either
 // follow a dot (e.g. foo.ba -> bar) or they are picked from globalCompletionList (e.g. fo -> foo)
@@ -7,7 +6,7 @@
 // consist of global constants that might not carry over from the calling environment.
 //
 // This function is likely the one you want to call from external modules
-function getCompletions(text, commandHeader, globalCompletionList) {
+export function getCompletions(text, commandHeader, globalCompletionList) {
     let methods = [];
     let expr_, base;
     let attrHead = '';
@@ -108,7 +107,7 @@ function findTheBrace(expr, offset, ...braces) {
 // There is no guarantee of correct javascript syntax between the return
 // value and offset.  This function is meant to take a string like
 // "foo(Obj.We.Are.Completing" and allow you to extract "Obj.We.Are.Completing"
-function getExpressionOffset(expr, offset) {
+export function getExpressionOffset(expr, offset) {
     while (offset >= 0) {
         let currChar = expr.charAt(offset);
 
@@ -132,7 +131,7 @@ function isValidPropertyName(w) {
 
 // To get all properties (enumerable and not), we need to walk
 // the prototype chain ourselves
-function getAllProps(obj) {
+export function getAllProps(obj) {
     if (obj === null || obj === undefined)
         return [];
 
@@ -144,7 +143,7 @@ function getAllProps(obj) {
 // e.g., expr="({ foo: null, bar: null, 4: null })" will
 // return ["foo", "bar", ...] but the list will not include "4",
 // since methods accessed with '.' notation must star with a letter or _.
-function getPropertyNamesFromExpression(expr, commandHeader = '') {
+export function getPropertyNamesFromExpression(expr, commandHeader = '') {
     let obj = {};
     if (!isUnsafeExpression(expr)) {
         try {
@@ -170,7 +169,7 @@ function getPropertyNamesFromExpression(expr, commandHeader = '') {
 }
 
 // Given a list of words, returns the longest prefix they all have in common
-function getCommonPrefix(words) {
+export function getCommonPrefix(words) {
     let word = words[0];
     for (let i = 0; i < word.length; i++) {
         for (let w = 1; w < words.length; w++) {
@@ -221,7 +220,7 @@ function isUnsafeExpression(str) {
 }
 
 // Returns a list of global keywords derived from str
-function getDeclaredConstants(str) {
+export function getDeclaredConstants(str) {
     let ret = [];
     str.split(';').forEach(s => {
         let base_, keyword;
diff --git a/js/misc/keyboardManager.js b/js/misc/keyboardManager.js
index 142e2f4198..f51224c91a 100644
--- a/js/misc/keyboardManager.js
+++ b/js/misc/keyboardManager.js
@@ -1,42 +1,43 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
-/* exported getKeyboardManager, holdKeyboard, releaseKeyboard */
 
-const { GLib, GnomeDesktop } = imports.gi;
+import GLib from 'gi://GLib';
+import GnomeDesktop from 'gi://GnomeDesktop';
 
-const Main = imports.ui.main;
+import Main from "../ui/main.js";
 
-var DEFAULT_LOCALE = 'en_US';
-var DEFAULT_LAYOUT = 'us';
-var DEFAULT_VARIANT = '';
+export let DEFAULT_LOCALE = 'en_US';
+export let DEFAULT_LAYOUT = 'us';
+export let DEFAULT_VARIANT = '';
 
 let _xkbInfo = null;
 
-function getXkbInfo() {
+export function getXkbInfo() {
     if (_xkbInfo == null)
         _xkbInfo = new GnomeDesktop.XkbInfo();
     return _xkbInfo;
 }
 
+/** @type {KeyboardManager | null} */
 let _keyboardManager = null;
 
-function getKeyboardManager() {
+export function getKeyboardManager() {
     if (_keyboardManager == null)
         _keyboardManager = new KeyboardManager();
     return _keyboardManager;
 }
 
-function releaseKeyboard() {
+export function releaseKeyboard() {
     if (Main.modalCount > 0)
         global.display.unfreeze_keyboard(global.get_current_time());
     else
         global.display.ungrab_keyboard(global.get_current_time());
 }
 
-function holdKeyboard() {
+export function holdKeyboard() {
     global.display.freeze_keyboard(global.get_current_time());
 }
 
-var KeyboardManager = class {
+export class KeyboardManager {
     constructor() {
         // The XKB protocol doesn't allow for more that 4 layouts in a
         // keymap. Wayland doesn't impose this limit and libxkbcommon can
diff --git a/js/misc/loginManager.js b/js/misc/loginManager.js
index f5ebaf6575..7f33763a6d 100644
--- a/js/misc/loginManager.js
+++ b/js/misc/loginManager.js
@@ -1,10 +1,10 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
-/* exported canLock, getLoginManager, registerSessionWithGDM */
 
-const { GLib, Gio } = imports.gi;
-const Signals = imports.misc.signals;
+import GLib from 'gi://GLib';
+import Gio from 'gi://Gio';
+import * as Signals from './signals.js';
 
-const { loadInterfaceXML } = imports.misc.fileUtils;
+import { loadInterfaceXML } from "./fileUtilsModule.js";
 
 const SystemdLoginManagerIface = loadInterfaceXML('org.freedesktop.login1.Manager');
 const SystemdLoginSessionIface = loadInterfaceXML('org.freedesktop.login1.Session');
@@ -32,7 +32,7 @@ function versionCompare(required, reference) {
     return true;
 }
 
-function canLock() {
+export function canLock() {
     try {
         let params = GLib.Variant.new('(ss)', ['org.gnome.DisplayManager.Manager', 'Version']);
         let result = Gio.DBus.system.call_sync('org.gnome.DisplayManager',
@@ -49,8 +49,7 @@ function canLock() {
     }
 }
 
-
-async function registerSessionWithGDM() {
+export async function registerSessionWithGDM() {
     log("Registering session with GDM");
     try {
         await Gio.DBus.system.call(
@@ -68,6 +67,7 @@ async function registerSessionWithGDM() {
     }
 }
 
+/** @type {LoginManagerSystemd | LoginManagerDummy | null} */
 let _loginManager = null;
 
 /**
@@ -76,7 +76,7 @@ let _loginManager = null;
  * @returns {object} - the LoginManager singleton
  *
  */
-function getLoginManager() {
+export function getLoginManager() {
     if (_loginManager == null) {
         if (haveSystemd())
             _loginManager = new LoginManagerSystemd();
@@ -87,7 +87,7 @@ function getLoginManager() {
     return _loginManager;
 }
 
-var LoginManagerSystemd = class extends Signals.EventEmitter {
+export class LoginManagerSystemd extends Signals.EventEmitter {
     constructor() {
         super();
 
@@ -205,7 +205,7 @@ var LoginManagerSystemd = class extends Signals.EventEmitter {
     }
 };
 
-var LoginManagerDummy = class extends Signals.EventEmitter  {
+export class LoginManagerDummy extends Signals.EventEmitter {
     getCurrentSessionProxy(_callback) {
         // we could return a DummySession object that fakes whatever callers
         // expect (at the time of writing: connect() and connectSignal()
diff --git a/js/misc/modemManager.js b/js/misc/modemManager.js
index 912acd349e..9c0c3b326e 100644
--- a/js/misc/modemManager.js
+++ b/js/misc/modemManager.js
@@ -1,9 +1,12 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported ModemBase, ModemGsm, ModemCdma, BroadbandModem  */
 
-const { Gio, GObject, NM, NMA } = imports.gi;
+import Gio from 'gi://Gio';
+import GObject from 'gi://GObject';
+import NM from 'gi://NM';
+import NMA from 'gi://NMA';
 
-const { loadInterfaceXML } = imports.misc.fileUtils;
+import { loadInterfaceXML } from "./fileUtilsModule.js";
 
 // _getMobileProvidersDatabase:
 //
@@ -98,7 +101,7 @@ const ModemGsmNetworkProxy = Gio.DBusProxy.makeProxyWrapper(ModemGsmNetworkInter
 const ModemCdmaInterface = loadInterfaceXML('org.freedesktop.ModemManager.Modem.Cdma');
 const ModemCdmaProxy = Gio.DBusProxy.makeProxyWrapper(ModemCdmaInterface);
 
-var ModemBase = GObject.registerClass({
+export const ModemBase = GObject.registerClass({
     GTypeFlags: GObject.TypeFlags.ABSTRACT,
     Properties: {
         'operator-name': GObject.ParamSpec.string(
@@ -111,8 +114,8 @@ var ModemBase = GObject.registerClass({
             0, 100, 0),
     },
 }, class ModemBase extends GObject.Object {
-    _init() {
-        super._init();
+    _init(...args) {
+        super._init(...args);
         this._operatorName = null;
         this._signalQuality = 0;
     }
@@ -140,7 +143,7 @@ var ModemBase = GObject.registerClass({
     }
 });
 
-var ModemGsm = GObject.registerClass(
+export const ModemGsm = GObject.registerClass(
 class ModemGsm extends ModemBase {
     _init(path) {
         super._init();
@@ -174,7 +177,7 @@ class ModemGsm extends ModemBase {
     }
 });
 
-var ModemCdma = GObject.registerClass(
+export const ModemCdma = GObject.registerClass(
 class ModemCdma extends ModemBase {
     _init(path) {
         super._init();
@@ -226,7 +229,7 @@ const BroadbandModem3gppProxy = Gio.DBusProxy.makeProxyWrapper(BroadbandModem3gp
 const BroadbandModemCdmaInterface = loadInterfaceXML('org.freedesktop.ModemManager1.Modem.ModemCdma');
 const BroadbandModemCdmaProxy = Gio.DBusProxy.makeProxyWrapper(BroadbandModemCdmaInterface);
 
-var BroadbandModem = GObject.registerClass({
+export const BroadbandModem = GObject.registerClass({
     Properties: {
         'capabilities': GObject.ParamSpec.flags(
             'capabilities', 'capabilities', 'capabilities',
diff --git a/js/misc/objectManager.js b/js/misc/objectManager.js
index a69f421ab2..b9a4038a60 100644
--- a/js/misc/objectManager.js
+++ b/js/misc/objectManager.js
@@ -1,8 +1,9 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported ObjectManager */
 
-const { Gio, GLib } = imports.gi;
-const Signals = imports.misc.signals;
+import Gio from 'gi://Gio';
+import GLib from 'gi://GLib';
+import * as Signals from './signals.js';
 
 // Specified in the D-Bus specification here:
 // http://dbus.freedesktop.org/doc/dbus-specification.html#standard-interfaces-objectmanager
@@ -25,7 +26,7 @@ const ObjectManagerIface = `
 
 const ObjectManagerInfo = Gio.DBusInterfaceInfo.new_for_xml(ObjectManagerIface);
 
-var ObjectManager = class extends Signals.EventEmitter {
+export class ObjectManager extends Signals.EventEmitter {
     constructor(params = {}) {
         super();
 
@@ -243,6 +244,9 @@ var ObjectManager = class extends Signals.EventEmitter {
         }
     }
 
+    /**
+     * @param {string[]} interfaces 
+     */
     _registerInterfaces(interfaces) {
         for (let i = 0; i < interfaces.length; i++) {
             let info = Gio.DBusInterfaceInfo.new_for_xml(interfaces[i]);
diff --git a/js/misc/params.d.ts b/js/misc/params.d.ts
new file mode 100644
index 0000000000..a1362722fc
--- /dev/null
+++ b/js/misc/params.d.ts
@@ -0,0 +1,6 @@
+export type Anyify<D extends { [key: string]: any }> = { [key in keyof D]?: any };
+
+export function parse<D extends { [key: string]: any }, P extends { [key: string]: any }>(params: P, 
defaults: D, allowExtras: true): D & typeof params;
+export function parse<D extends { [key: string]: any }, P extends Anyify<D>>(params: P, defaults: D, 
allowExtras: false): D;
+export function parse<D extends { [key: string]: any }, P extends Anyify<D>>(params: P, defaults: D): D;
+export function parse<D extends { [key: string]: any }, P extends Anyify<D> | { [key: string]: any 
}>(params: P, defaults: D, allowExtras: boolean): D | D & typeof params;
diff --git a/js/misc/params.js b/js/misc/params.js
index 817d66ceb8..32ddbf692f 100644
--- a/js/misc/params.js
+++ b/js/misc/params.js
@@ -15,7 +15,7 @@
 //
 // Return value: a new object, containing the merged parameters from
 // @params and @defaults
-function parse(params = {}, defaults, allowExtras) {
+export function parse(params = {}, defaults, allowExtras) {
     if (!allowExtras) {
         for (let prop in params) {
             if (!(prop in defaults))
diff --git a/js/misc/parentalControlsManager.js b/js/misc/parentalControlsManager.js
index fc1e7ced6f..5b42e07153 100644
--- a/js/misc/parentalControlsManager.js
+++ b/js/misc/parentalControlsManager.js
@@ -21,23 +21,24 @@
 // along with this program; if not, write to the Free Software
 // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 
-/* exported getDefault */
+import Gio from 'gi://Gio';
+import GObject from 'gi://GObject';
+import Shell from 'gi://Shell';
 
-const { Gio, GObject, Shell } = imports.gi;
+import gi from 'gi';
 
 // We require libmalcontent ≥ 0.6.0
-const HAVE_MALCONTENT = imports.package.checkSymbol(
-    'Malcontent', '0', 'ManagerGetValueFlags');
+const HAVE_MALCONTENT = true;
 
-var Malcontent = null;
+let Malcontent = null;
 if (HAVE_MALCONTENT) {
-    Malcontent = imports.gi.Malcontent;
+    Malcontent = gi.require('Malcontent');
     Gio._promisify(Malcontent.Manager.prototype, 'get_app_filter_async', 'get_app_filter_finish');
 }
 
 let _singleton = null;
 
-function getDefault() {
+export function getDefault() {
     if (_singleton === null)
         _singleton = new ParentalControlsManager();
 
@@ -48,7 +49,7 @@ function getDefault() {
 // parental controls settings. It’s possible for the user’s parental controls
 // to change at runtime if the Parental Controls application is used by an
 // administrator from within the user’s session.
-var ParentalControlsManager = GObject.registerClass({
+export const ParentalControlsManager = GObject.registerClass({
     Signals: {
         'app-filter-changed': {},
     },
diff --git a/js/misc/permissionStore.js b/js/misc/permissionStore.js
index 14fc47f7e8..368355406d 100644
--- a/js/misc/permissionStore.js
+++ b/js/misc/permissionStore.js
@@ -1,14 +1,14 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported PermissionStore */
 
-const Gio = imports.gi.Gio;
+import Gio from 'gi://Gio';
 
-const { loadInterfaceXML } = imports.misc.fileUtils;
+import { loadInterfaceXML } from "./fileUtilsModule.js";
 
 const PermissionStoreIface = loadInterfaceXML('org.freedesktop.impl.portal.PermissionStore');
 const PermissionStoreProxy = Gio.DBusProxy.makeProxyWrapper(PermissionStoreIface);
 
-function PermissionStore(initCallback, cancellable) {
+export function PermissionStore(initCallback, cancellable) {
     return PermissionStoreProxy(Gio.DBus.session,
                                 'org.freedesktop.impl.portal.PermissionStore',
                                 '/org/freedesktop/impl/portal/PermissionStore',
diff --git a/js/misc/signals.d.ts b/js/misc/signals.d.ts
new file mode 100644
index 0000000000..6161980485
--- /dev/null
+++ b/js/misc/signals.d.ts
@@ -0,0 +1,9 @@
+export type EventId = number;
+
+export class EventEmitter {
+  connect(event: string, handler: (...args: any[]) => any): EventId;
+  disconnect(id: EventId);
+  emit(event: string, ...args: any[]);
+  disconnectAll();
+  signalHandlerIsConnected(event: string);
+}
diff --git a/js/misc/signals.js b/js/misc/signals.js
index 48b3429f3f..2eb6d32541 100644
--- a/js/misc/signals.js
+++ b/js/misc/signals.js
@@ -1,5 +1,6 @@
+/** @type {import("environment").SignalsNamespace} */
 const Signals = imports.signals;
 
-var EventEmitter = class EventEmitter {};
+export class EventEmitter {}
 
 Signals.addSignalMethods(EventEmitter.prototype);
diff --git a/js/misc/smartcardManager.js b/js/misc/smartcardManager.js
index 0e03ab86d9..75ed354bd3 100644
--- a/js/misc/smartcardManager.js
+++ b/js/misc/smartcardManager.js
@@ -1,10 +1,9 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
-/* exported getSmartcardManager */
 
-const Gio = imports.gi.Gio;
-const Signals = imports.misc.signals;
+import Gio from 'gi://Gio';
+import * as Signals from './signals.js';
 
-const ObjectManager = imports.misc.objectManager;
+import * as ObjectManager from "./objectManager.js";
 
 const SmartcardTokenIface = `
 <node>
@@ -16,16 +15,17 @@ const SmartcardTokenIface = `
 </interface>
 </node>`;
 
+/** @type {SmartcardManager | null} */
 let _smartcardManager = null;
 
-function getSmartcardManager() {
+export function getSmartcardManager() {
     if (_smartcardManager == null)
         _smartcardManager = new SmartcardManager();
 
     return _smartcardManager;
 }
 
-var SmartcardManager = class extends Signals.EventEmitter {
+export class SmartcardManager extends Signals.EventEmitter {
     constructor() {
         super();
 
diff --git a/js/misc/systemActions.js b/js/misc/systemActions.js
index 10cab9a9e2..fba24c3713 100644
--- a/js/misc/systemActions.js
+++ b/js/misc/systemActions.js
@@ -1,9 +1,17 @@
 /* exported getDefault */
-const { AccountsService, Clutter, Gdm, Gio, GLib, GObject, Meta } = imports.gi;
+import AccountsService from 'gi://AccountsService';
+import Clutter from 'gi://Clutter';
+import Gdm from 'gi://Gdm';
+import Gio from 'gi://Gio';
+import GLib from 'gi://GLib';
+import GObject from 'gi://GObject';
+import Meta from 'gi://Meta';
 
-const GnomeSession = imports.misc.gnomeSession;
-const LoginManager = imports.misc.loginManager;
-const Main = imports.ui.main;
+
+import * as GnomeSession from "./gnomeSession.js";
+import * as LoginManager from "./loginManager.js";
+
+import Main from "../ui/main.js";
 
 const LOCKDOWN_SCHEMA = 'org.gnome.desktop.lockdown';
 const LOGIN_SCREEN_SCHEMA = 'org.gnome.login-screen';
@@ -23,7 +31,7 @@ const LOCK_ORIENTATION_ACTION_ID = 'lock-orientation';
 
 let _singleton = null;
 
-function getDefault() {
+export function getDefault() {
     if (_singleton == null)
         _singleton = new SystemActions();
 
@@ -137,7 +145,8 @@ const SystemActions = GObject.registerClass({
         this._lockdownSettings = new Gio.Settings({ schema_id: LOCKDOWN_SCHEMA });
         this._orientationSettings = new Gio.Settings({ schema_id: 
'org.gnome.settings-daemon.peripherals.touchscreen' });
 
-        this._session = new GnomeSession.SessionManager();
+        // FIXME
+        this._session = GnomeSession.SessionManager();
         this._loginManager = LoginManager.getLoginManager();
         this._monitorManager = Meta.MonitorManager.get();
 
diff --git a/js/misc/util.js b/js/misc/util.js
index dd25eacce9..4a54606e41 100644
--- a/js/misc/util.js
+++ b/js/misc/util.js
@@ -4,12 +4,18 @@
             ensureActorVisibleInScrollView, wiggle, lerp, GNOMEversionCompare,
             DBusSenderChecker */
 
-const { Clutter, Gio, GLib, Shell, St, GnomeDesktop } = imports.gi;
-const Gettext = imports.gettext;
+import Clutter from 'gi://Clutter';
+import Gio from 'gi://Gio';
+import GLib from 'gi://GLib';
+import Shell from 'gi://Shell';
+import St from 'gi://St';
+import GnomeDesktop from 'gi://GnomeDesktop';
 
-const Main = imports.ui.main;
+import Main from "../ui/main.js";
 
-var SCROLL_TIME = 100;
+import * as Gettext from 'gettext';
+
+export let SCROLL_TIME = 100;
 
 const WIGGLE_OFFSET = 6;
 const WIGGLE_DURATION = 65;
@@ -52,7 +58,7 @@ let _desktopSettings = null;
 // the position within @str where the URL was found.
 //
 // Return value: the list of match objects, as described above
-function findUrls(str) {
+export function findUrls(str) {
     let res = [], match;
     while ((match = _urlRegexp.exec(str)))
         res.push({ url: match[2], pos: match.index + match[1].length });
@@ -64,7 +70,7 @@ function findUrls(str) {
 //
 // Runs @argv in the background, handling any errors that occur
 // when trying to start the program.
-function spawn(argv) {
+export function spawn(argv) {
     try {
         trySpawn(argv);
     } catch (err) {
@@ -77,7 +83,7 @@ function spawn(argv) {
 //
 // Runs @commandLine in the background, handling any errors that
 // occur when trying to parse or start the program.
-function spawnCommandLine(commandLine) {
+export function spawnCommandLine(commandLine) {
     try {
         let [success_, argv] = GLib.shell_parse_argv(commandLine);
         trySpawn(argv);
@@ -90,7 +96,7 @@ function spawnCommandLine(commandLine) {
 // @argv: an argv array
 //
 // Runs @argv as if it was an application, handling startup notification
-function spawnApp(argv) {
+export function spawnApp(argv) {
     try {
         let app = Gio.AppInfo.create_from_commandline(argv.join(' '), null,
                                                       Gio.AppInfoCreateFlags.SUPPORTS_STARTUP_NOTIFICATION);
@@ -108,7 +114,7 @@ function spawnApp(argv) {
 // Runs @argv in the background. If launching @argv fails,
 // this will throw an error.
 function trySpawn(argv) {
-    var success_, pid;
+    let success_, pid;
     try {
         [success_, pid] = GLib.spawn_async(null, argv, null,
                                            GLib.SpawnFlags.SEARCH_PATH | GLib.SpawnFlags.DO_NOT_REAP_CHILD,
@@ -145,7 +151,7 @@ function trySpawn(argv) {
 //
 // Runs @commandLine in the background. If launching @commandLine
 // fails, this will throw an error.
-function trySpawnCommandLine(commandLine) {
+export function trySpawnCommandLine(commandLine) {
     let success_, argv;
 
     try {
@@ -165,7 +171,7 @@ function _handleSpawnError(command, err) {
     Main.notifyError(title, err.message);
 }
 
-function formatTimeSpan(date) {
+export function formatTimeSpan(date) {
     let now = GLib.DateTime.new_now_local();
 
     let timespan = now.difference(date);
@@ -205,7 +211,17 @@ function formatTimeSpan(date) {
                             "%d years ago", yearsAgo).format(yearsAgo);
 }
 
-function formatTime(time, params = {}) {
+/** 
+ * @typedef {object} FormatTimeProps
+ * @property {boolean} timeOnly
+ * @property {boolean} ampm
+ */
+
+/**
+ * @param {GLib.DateTime | Date} time
+ * @param {Partial<FormatTimeProps>} params
+ */
+export function formatTime(time, params) {
     let date;
     // HACK: The built-in Date type sucks at timezones, which we need for the
     //       world clock; it's often more convenient though, so allow either
@@ -300,7 +316,7 @@ function formatTime(time, params = {}) {
     return formattedTime.replace(/([:\u2236])/g, '\u200e$1');
 }
 
-function createTimeLabel(date, params) {
+export function createTimeLabel(date, params) {
     if (_desktopSettings == null)
         _desktopSettings = new Gio.Settings({ schema_id: 'org.gnome.desktop.interface' });
 
@@ -356,14 +372,14 @@ function lowerBound(array, val, cmp) {
 // Inserts @val into @array, preserving the
 // sorting invariants.
 // Returns the position at which it was inserted
-function insertSorted(array, val, cmp) {
+export function insertSorted(array, val, cmp) {
     let pos = lowerBound(array, val, cmp);
     array.splice(pos, 0, val);
 
     return pos;
 }
 
-function ensureActorVisibleInScrollView(scrollView, actor) {
+export function ensureActorVisibleInScrollView(scrollView, actor) {
     let adjustment = scrollView.vscroll.adjustment;
     let [value, lower_, upper, stepIncrement_, pageIncrement_, pageSize] = adjustment.get_values();
 
@@ -399,7 +415,18 @@ function ensureActorVisibleInScrollView(scrollView, actor) {
     });
 }
 
-function wiggle(actor, params = {}) {
+/** 
+ * @typedef {object} WiggleProps
+ * @property {number} offset
+ * @property {number} duration
+ * @property {number} wiggleCount
+ */
+
+/**
+ * @param {Clutter.Actor} actor
+ * @param {Partial<WiggleProps>} [params]
+ */
+export function wiggle(actor, params = {}) {
     if (!St.Settings.get().enable_animations)
         return;
 
@@ -436,7 +463,7 @@ function wiggle(actor, params = {}) {
     });
 }
 
-function lerp(start, end, progress) {
+export function lerp(start, end, progress) {
     return start + progress * (end - start);
 }
 
@@ -462,7 +489,7 @@ function _GNOMEversionToNumber(version) {
 //
 // Returns an integer less than, equal to, or greater than
 // zero, if version1 is older, equal or newer than version2
-function GNOMEversionCompare(version1, version2) {
+export function GNOMEversionCompare(version1, version2) {
     const v1Array = version1.split('.');
     const v2Array = version2.split('.');
 
@@ -478,7 +505,7 @@ function GNOMEversionCompare(version1, version2) {
     return 0;
 }
 
-var DBusSenderChecker = class {
+export class DBusSenderChecker {
     /**
      * @param {string[]} allowList - list of allowed well-known names
      */
diff --git a/js/misc/weather.js b/js/misc/weather.js
index 5d6cca1ea1..eca6c3d786 100644
--- a/js/misc/weather.js
+++ b/js/misc/weather.js
@@ -1,12 +1,16 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported WeatherClient */
 
-const { Geoclue, Gio, GLib, GWeather, Shell } = imports.gi;
-const Signals = imports.misc.signals;
+import Geoclue from 'gi://Geoclue';
+import Gio from 'gi://Gio';
+import GLib from 'gi://GLib';
+import GWeather from 'gi://GWeather';
+import Shell from 'gi://Shell';
+import * as Signals from './signals.js';
 
-const PermissionStore = imports.misc.permissionStore;
+import * as PermissionStore from "./permissionStore.js";
 
-const { loadInterfaceXML } = imports.misc.fileUtils;
+import { loadInterfaceXML } from "./fileUtilsModule.js";
 
 Gio._promisify(Geoclue.Simple, 'new', 'new_finish');
 
@@ -18,10 +22,11 @@ const WEATHER_INTEGRATION_IFACE = 'org.gnome.Shell.WeatherIntegration';
 
 const WEATHER_APP_ID = 'org.gnome.Weather.desktop';
 
+export let UPDATE_THRESHOLD = 10 * GLib.TIME_SPAN_MINUTE;
+
 // Minimum time between updates to show loading indication
-var UPDATE_THRESHOLD = 10 * GLib.TIME_SPAN_MINUTE;
 
-var WeatherClient = class extends Signals.EventEmitter {
+export class WeatherClient extends Signals.EventEmitter {
     constructor() {
         super();
 
@@ -293,7 +298,7 @@ var WeatherClient = class extends Signals.EventEmitter {
     }
 
     _onLocationsChanged() {
-        let locations = this._settings.get_value('locations').deep_unpack();
+        let locations = /** @type {GLib.Variant<'av'>} */ 
(this._settings.get_value('locations')).deep_unpack();
         let serialized = locations.shift();
         let mostRecentLocation = null;
 
diff --git a/js/perf/basic.js b/js/perf/basic.js
index 718532f285..9f05c6d766 100644
--- a/js/perf/basic.js
+++ b/js/perf/basic.js
@@ -5,15 +5,16 @@
 */
 /* eslint camelcase: ["error", { properties: "never", allow: ["^script_"] }] */
 
-const { St } = imports.gi;
+import St from 'gi://St';
 
-const Main = imports.ui.main;
-const MessageTray = imports.ui.messageTray;
-const Scripting = imports.ui.scripting;
+import Main from "../ui/main.js";
+
+import * as MessageTray from "../ui/messageTray.js";
+import * as Scripting from "../ui/scripting.js";
 
 // This script tests the most important (basic) functionality of the shell.
 
-var METRICS = {};
+export const METRICS = {};
 
 async function run() {
     /* eslint-disable no-await-in-loop */
diff --git a/js/perf/core.js b/js/perf/core.js
index f3f496b030..da5ba378fd 100644
--- a/js/perf/core.js
+++ b/js/perf/core.js
@@ -5,17 +5,17 @@
             clutter_stagePaintDone */
 /* eslint camelcase: ["error", { properties: "never", allow: ["^script_", "^malloc", "^glx", "^clutter"] }] 
*/
 
-const System = imports.system;
+import System from 'system';
 
-const Main = imports.ui.main;
-const Scripting = imports.ui.scripting;
+import Main from "../ui/main.js";
+import * as Scripting from "../ui/scripting.js";
 
 // This performance script measure the most important (core) performance
 // metrics for the shell. By looking at the output metrics of this script
 // someone should be able to get an idea of how well the shell is performing
 // on a particular system.
 
-var METRICS = {
+export const METRICS = {
     overviewLatencyFirst:
     { description: "Time to first frame after triggering overview, first time",
       units: "us" },
diff --git a/js/perf/hwtest.js b/js/perf/hwtest.js
index 0f396acd32..321e619602 100644
--- a/js/perf/hwtest.js
+++ b/js/perf/hwtest.js
@@ -7,11 +7,14 @@
             script_geditLaunch, script_geditFirstFrame,
             clutter_stagePaintStart, clutter_paintCompletedTimestamp */
 /* eslint camelcase: ["error", { properties: "never", allow: ["^script_", "^clutter"] }] */
-const { Clutter, Gio, Shell } = imports.gi;
-const Main = imports.ui.main;
-const Scripting = imports.ui.scripting;
+import Clutter from 'gi://Clutter';
+import Gio from 'gi://Gio';
+import Shell from 'gi://Shell';
 
-var METRICS = {
+import Main from "../ui/main.js";
+import * as Scripting from "../ui/scripting.js";
+
+export const METRICS = {
     timeToDesktop:
     { description: "Time from starting graphical.target to desktop showing",
       units: "us" },
diff --git a/js/ui/accessDialog.js b/js/ui/accessDialog.js
index 7c8d69b806..dac1ed50d3 100644
--- a/js/ui/accessDialog.js
+++ b/js/ui/accessDialog.js
@@ -1,22 +1,29 @@
 /* exported AccessDialogDBus */
-const { Clutter, Gio, GLib, GObject, Shell, St } = imports.gi;
+import Clutter from 'gi://Clutter';
+import Gio from 'gi://Gio';
+import GLib from 'gi://GLib';
+import GObject from 'gi://GObject';
+import Shell from 'gi://Shell';
+import St from 'gi://St';
 
-const CheckBox = imports.ui.checkBox;
-const Dialog = imports.ui.dialog;
-const ModalDialog = imports.ui.modalDialog;
 
-const { loadInterfaceXML } = imports.misc.fileUtils;
+import * as CheckBox from './checkBox.js';
+import * as Dialog from './dialog.js';
+import * as ModalDialog from './modalDialog.js';
+
+import { loadInterfaceXML } from '../misc/fileUtilsModule.js';
 
 const RequestIface = loadInterfaceXML('org.freedesktop.impl.portal.Request');
 const AccessIface = loadInterfaceXML('org.freedesktop.impl.portal.Access');
 
-var DialogResponse = {
+/** @enum {number} */
+export const DialogResponse = {
     OK: 0,
     CANCEL: 1,
     CLOSED: 2,
 };
 
-var AccessDialog = GObject.registerClass(
+export const AccessDialog = GObject.registerClass(
 class AccessDialog extends ModalDialog.ModalDialog {
     _init(invocation, handle, title, description, body, options) {
         super._init({ styleClass: 'access-dialog' });
@@ -100,6 +107,7 @@ class AccessDialog extends ModalDialog.ModalDialog {
             this._request.unexport();
         this._requestExported = false;
 
+        /** @type {{ [key: string]: GLib.Variant<'s'> }} */
         let results = {};
         if (response == DialogResponse.OK) {
             for (let [id, check] of this._choices) {
@@ -117,7 +125,7 @@ class AccessDialog extends ModalDialog.ModalDialog {
     }
 });
 
-var AccessDialogDBus = class {
+export class AccessDialogDBus {
     constructor() {
         this._accessDialog = null;
 
diff --git a/js/ui/altTab.js b/js/ui/altTab.js
index 6af9380edc..f428f3fbb2 100644
--- a/js/ui/altTab.js
+++ b/js/ui/altTab.js
@@ -2,24 +2,33 @@
 /* exported AppSwitcherPopup, GroupCyclerPopup, WindowSwitcherPopup,
             WindowCyclerPopup */
 
-const { Atk, Clutter, Gio, GLib, GObject, Meta, Shell, St } = imports.gi;
+import Atk from 'gi://Atk';
+import Clutter from 'gi://Clutter';
+import Gio from 'gi://Gio';
+import GLib from 'gi://GLib';
+import GObject from 'gi://GObject';
+import Meta from 'gi://Meta';
+import Shell from 'gi://Shell';
+import St from 'gi://St';
 
-const Main = imports.ui.main;
-const SwitcherPopup = imports.ui.switcherPopup;
 
-var APP_ICON_HOVER_TIMEOUT = 200; // milliseconds
+import Main from './main.js';
+import * as SwitcherPopup from './switcherPopup.js';
 
-var THUMBNAIL_DEFAULT_SIZE = 256;
-var THUMBNAIL_POPUP_TIME = 500; // milliseconds
-var THUMBNAIL_FADE_TIME = 100; // milliseconds
+export let APP_ICON_HOVER_TIMEOUT = 200; // milliseconds
 
-var WINDOW_PREVIEW_SIZE = 128;
-var APP_ICON_SIZE = 96;
-var APP_ICON_SIZE_SMALL = 48;
+export let THUMBNAIL_DEFAULT_SIZE = 256;
+export let THUMBNAIL_POPUP_TIME = 500; // milliseconds
+export let THUMBNAIL_FADE_TIME = 100; // milliseconds
+
+export let WINDOW_PREVIEW_SIZE = 128;
+export let APP_ICON_SIZE = 96;
+export let APP_ICON_SIZE_SMALL = 48;
 
 const baseIconSizes = [96, 64, 48, 32, 22];
 
-var AppIconMode = {
+/** @enum {number} */
+export const AppIconMode = {
     THUMBNAIL_ONLY: 1,
     APP_ICON_ONLY: 2,
     BOTH: 3,
@@ -38,7 +47,7 @@ function _createWindowClone(window, size) {
                                y_expand: true });
 }
 
-function getWindows(workspace) {
+export function getWindows(workspace) {
     // We ignore skip-taskbar windows in switchers, but if they are attached
     // to their parent, their position in the MRU list may be more appropriate
     // than the parent; so start with the complete list ...
@@ -51,7 +60,7 @@ function getWindows(workspace) {
     }).filter((w, i, a) => !w.skip_taskbar && a.indexOf(w) == i);
 }
 
-var AppSwitcherPopup = GObject.registerClass(
+export const AppSwitcherPopup = GObject.registerClass(
 class AppSwitcherPopup extends SwitcherPopup.SwitcherPopup {
     _init() {
         super._init();
@@ -290,8 +299,8 @@ class AppSwitcherPopup extends SwitcherPopup.SwitcherPopup {
     /**
      * _select:
      * @param {number} app: index of the app to select
-     * @param {number=} window: index of which of @app's windows to select
-     * @param {bool} forceAppFocus: optional flag, see below
+     * @param {number} [window]: index of which of @app's windows to select
+     * @param {boolean} [forceAppFocus]: optional flag, see below
      *
      * Selects the indicated @app, and optional @window, and sets
      * this._thumbnailsFocused appropriately to indicate whether the
@@ -397,13 +406,14 @@ class AppSwitcherPopup extends SwitcherPopup.SwitcherPopup {
     }
 });
 
-var CyclerHighlight = GObject.registerClass(
+export const CyclerHighlight = GObject.registerClass(
 class CyclerHighlight extends St.Widget {
     _init() {
         super._init({ layout_manager: new Clutter.BinLayout() });
         this._window = null;
         this._sizeChangedId = 0;
 
+        /** @type {Clutter.Clone<Meta.WindowActor>} */
         this._clone = new Clutter.Clone();
         this.add_actor(this._clone);
 
@@ -465,7 +475,7 @@ class CyclerHighlight extends St.Widget {
 
 // We don't show an actual popup, so just provide what SwitcherPopup
 // expects instead of inheriting from SwitcherList
-var CyclerList = GObject.registerClass({
+export const CyclerList = GObject.registerClass({
     Signals: { 'item-activated': { param_types: [GObject.TYPE_INT] },
                'item-entered': { param_types: [GObject.TYPE_INT] },
                'item-removed': { param_types: [GObject.TYPE_INT] },
@@ -476,7 +486,7 @@ var CyclerList = GObject.registerClass({
     }
 });
 
-var CyclerPopup = GObject.registerClass({
+export const CyclerPopup = GObject.registerClass({
     GTypeFlags: GObject.TypeFlags.ABSTRACT,
 }, class CyclerPopup extends SwitcherPopup.SwitcherPopup {
     _init() {
@@ -493,6 +503,13 @@ var CyclerPopup = GObject.registerClass({
         });
     }
 
+    /**
+     * @returns {Meta.Window[]}
+     */
+    _getWindows() {
+        throw new GObject.NotImplementedError(`_getWindows in ${this.constructor.name}`);
+    }
+
     _highlightItem(index, _justOutline) {
         this._highlight.window = this._items[index];
         global.window_group.set_child_above_sibling(this._highlight, null);
@@ -532,7 +549,7 @@ var CyclerPopup = GObject.registerClass({
 });
 
 
-var GroupCyclerPopup = GObject.registerClass(
+export const GroupCyclerPopup = GObject.registerClass(
 class GroupCyclerPopup extends CyclerPopup {
     _init() {
         this._settings = new Gio.Settings({ schema_id: 'org.gnome.shell.app-switcher' });
@@ -565,7 +582,7 @@ class GroupCyclerPopup extends CyclerPopup {
     }
 });
 
-var WindowSwitcherPopup = GObject.registerClass(
+export const WindowSwitcherPopup = GObject.registerClass(
 class WindowSwitcherPopup extends SwitcherPopup.SwitcherPopup {
     _init() {
         super._init();
@@ -623,7 +640,7 @@ class WindowSwitcherPopup extends SwitcherPopup.SwitcherPopup {
     }
 });
 
-var WindowCyclerPopup = GObject.registerClass(
+export const WindowCyclerPopup = GObject.registerClass(
 class WindowCyclerPopup extends CyclerPopup {
     _init() {
         this._settings = new Gio.Settings({ schema_id: 'org.gnome.shell.window-switcher' });
@@ -654,12 +671,15 @@ class WindowCyclerPopup extends CyclerPopup {
     }
 });
 
-var AppIcon = GObject.registerClass(
+export const AppIcon = GObject.registerClass(
 class AppIcon extends St.BoxLayout {
     _init(app) {
         super._init({ style_class: 'alt-tab-app',
                       vertical: true });
 
+        /** @type {Meta.Window[]} */
+        this.cachedWindows = [];
+
         this.app = app;
         this.icon = null;
         this._iconBin = new St.Bin();
@@ -679,7 +699,7 @@ class AppIcon extends St.BoxLayout {
     }
 });
 
-var AppSwitcher = GObject.registerClass(
+export const AppSwitcher = GObject.registerClass(
 class AppSwitcher extends SwitcherPopup.SwitcherList {
     _init(apps, altTabPopup) {
         super._init(true);
@@ -814,6 +834,8 @@ class AppSwitcher extends SwitcherPopup.SwitcherList {
         } else {
             this._itemEntered(index);
         }
+
+        return false;
     }
 
     _enterItem(index) {
@@ -884,7 +906,7 @@ class AppSwitcher extends SwitcherPopup.SwitcherList {
     }
 });
 
-var ThumbnailSwitcher = GObject.registerClass(
+export const ThumbnailSwitcher = GObject.registerClass(
 class ThumbnailSwitcher extends SwitcherPopup.SwitcherList {
     _init(windows) {
         super._init(false);
@@ -892,6 +914,7 @@ class ThumbnailSwitcher extends SwitcherPopup.SwitcherList {
         this._labels = [];
         this._thumbnailBins = [];
         this._clones = [];
+        /** @type {Meta.Window[]} */
         this._windows = windows;
 
         for (let i = 0; i < windows.length; i++) {
@@ -979,7 +1002,7 @@ class ThumbnailSwitcher extends SwitcherPopup.SwitcherList {
     }
 });
 
-var WindowIcon = GObject.registerClass(
+export const WindowIcon = GObject.registerClass(
 class WindowIcon extends St.BoxLayout {
     _init(window, mode) {
         super._init({ style_class: 'alt-tab-app',
@@ -987,6 +1010,8 @@ class WindowIcon extends St.BoxLayout {
 
         this.window = window;
 
+        this._unmanagedSignalId = -1;
+
         this._icon = new St.Widget({ layout_manager: new Clutter.BinLayout() });
 
         this.add_child(this._icon);
@@ -1037,7 +1062,7 @@ class WindowIcon extends St.BoxLayout {
     }
 });
 
-var WindowSwitcher = GObject.registerClass(
+export const WindowSwitcher = GObject.registerClass(
 class WindowSwitcher extends SwitcherPopup.SwitcherList {
     _init(windows, mode) {
         super._init(true);
@@ -1070,6 +1095,9 @@ class WindowSwitcher extends SwitcherPopup.SwitcherList {
         });
     }
 
+    /**
+     * @returns {[number, number]}
+     */
     vfunc_get_preferred_height(forWidth) {
         let [minHeight, natHeight] = super.vfunc_get_preferred_height(forWidth);
 
diff --git a/js/ui/animation.js b/js/ui/animation.js
index b81eb2b904..f98a13f978 100644
--- a/js/ui/animation.js
+++ b/js/ui/animation.js
@@ -1,14 +1,26 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported Animation, AnimatedIcon, Spinner */
 
-const { Clutter, GLib, GObject, Gio, St } = imports.gi;
-
-
-var ANIMATED_ICON_UPDATE_TIMEOUT = 16;
-var SPINNER_ANIMATION_TIME = 300;
-var SPINNER_ANIMATION_DELAY = 1000;
-
-var Animation = GObject.registerClass(
+import Clutter from 'gi://Clutter';
+import GLib from 'gi://GLib';
+import GObject from 'gi://GObject';
+import Gio from 'gi://Gio';
+import St from 'gi://St';
+
+
+export let ANIMATED_ICON_UPDATE_TIMEOUT = 16;
+export let SPINNER_ANIMATION_TIME = 300;
+export let SPINNER_ANIMATION_DELAY = 1000;
+
+/**
+ * @typedef {object} AnimationParams
+ * @property {Gio.File} file
+ * @property {number} width
+ * @property {number} height
+ * @property {number} speed
+ */
+
+export const Animation = GObject.registerClass(
 class Animation extends St.Bin {
     _init(file, width, height, speed) {
         const themeContext = St.ThemeContext.get_for_stage(global.stage);
@@ -129,15 +141,26 @@ class Animation extends St.Bin {
     }
 });
 
-var AnimatedIcon = GObject.registerClass(
+export const AnimatedIcon = GObject.registerClass(
 class AnimatedIcon extends Animation {
     _init(file, size) {
         super._init(file, size, size, ANIMATED_ICON_UPDATE_TIMEOUT);
     }
 });
 
-var Spinner = GObject.registerClass(
+export const Spinner = GObject.registerClass(
 class Spinner extends AnimatedIcon {
+    /** 
+     * @typedef {object} SpinnerParams
+     * @property {boolean} [animate]
+     * @property {boolean} [hideOnStop]
+     */
+
+    /**
+     * _init:
+     * @param {number} size
+     * @param {SpinnerParams} [params]
+     */    
     _init(size, params = {}) {
         const {
             animate = false,
diff --git a/js/ui/appDisplay.js b/js/ui/appDisplay.js
index 984844f117..0c2c3c9d84 100644
--- a/js/ui/appDisplay.js
+++ b/js/ui/appDisplay.js
@@ -1,39 +1,45 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported AppDisplay, AppSearchProvider */
 
-const { Clutter, Gio, GLib, GObject, Graphene, Meta,
-    Pango, Shell, St } = imports.gi;
-
-const AppFavorites = imports.ui.appFavorites;
-const { AppMenu } = imports.ui.appMenu;
-const BoxPointer = imports.ui.boxpointer;
-const DND = imports.ui.dnd;
-const GrabHelper = imports.ui.grabHelper;
-const IconGrid = imports.ui.iconGrid;
-const Layout = imports.ui.layout;
-const Main = imports.ui.main;
-const PageIndicators = imports.ui.pageIndicators;
-const ParentalControlsManager = imports.misc.parentalControlsManager;
-const PopupMenu = imports.ui.popupMenu;
-const Search = imports.ui.search;
-const SwipeTracker = imports.ui.swipeTracker;
-const SystemActions = imports.misc.systemActions;
-
-var MENU_POPUP_TIMEOUT = 600;
-var POPDOWN_DIALOG_TIMEOUT = 500;
-
-var FOLDER_SUBICON_FRACTION = .4;
-
-var VIEWS_SWITCH_TIME = 400;
-var VIEWS_SWITCH_ANIMATION_DELAY = 100;
-
-var SCROLL_TIMEOUT_TIME = 150;
-
-var APP_ICON_SCALE_IN_TIME = 500;
-var APP_ICON_SCALE_IN_DELAY = 700;
-
-var APP_ICON_TITLE_EXPAND_TIME = 200;
-var APP_ICON_TITLE_COLLAPSE_TIME = 100;
+import Clutter from 'gi://Clutter';
+import GLib from 'gi://GLib';
+import GObject from 'gi://GObject';
+import Gio from 'gi://Gio';
+
+import Graphene from 'gi://Graphene';
+import Pango from 'gi://Pango';
+import Meta from 'gi://Meta';
+import Shell from 'gi://Shell';
+import St from 'gi://St';
+
+import * as AppFavorites from './appFavorites.js';
+import * as DND from './dnd.js';
+import * as GrabHelper from './grabHelper.js';
+import * as IconGrid from './iconGrid.js';
+import * as Layout from './layout.js';
+import Main from './main.js';
+import * as PageIndicators from './pageIndicators.js';
+import * as ParentalControlsManager from '../misc/parentalControlsManager.js';
+import * as PopupMenu from './popupMenu.js';
+import * as Search from './search.js';
+import * as SwipeTracker from './swipeTracker.js';
+import * as SystemActions from '../misc/systemActions.js';
+
+export const MENU_POPUP_TIMEOUT = 600;
+export const POPDOWN_DIALOG_TIMEOUT = 500;
+
+export const FOLDER_SUBICON_FRACTION = .4;
+
+export const VIEWS_SWITCH_TIME = 400;
+export const VIEWS_SWITCH_ANIMATION_DELAY = 100;
+
+export const SCROLL_TIMEOUT_TIME = 150;
+
+export const APP_ICON_SCALE_IN_TIME = 500;
+export const APP_ICON_SCALE_IN_DELAY = 700;
+
+export const APP_ICON_TITLE_EXPAND_TIME = 200;
+export const APP_ICON_TITLE_COLLAPSE_TIME = 100;
 
 const FOLDER_DIALOG_ANIMATION_TIME = 200;
 
@@ -86,14 +92,9 @@ function _getFolderName(folder) {
     return name;
 }
 
-function _getViewFromIcon(icon) {
-    for (let parent = icon.get_parent(); parent; parent = parent.get_parent()) {
-        if (parent instanceof BaseAppView)
-            return parent;
-    }
-    return null;
-}
-
+/**
+ * @param {readonly Shell.App[]} apps 
+ */
 function _findBestFolderName(apps) {
     let appInfos = apps.map(app => app.get_app_info());
 
@@ -126,7 +127,7 @@ function _findBestFolderName(apps) {
     return null;
 }
 
-var BaseAppView = GObject.registerClass({
+export const BaseAppView = GObject.registerClass({
     GTypeFlags: GObject.TypeFlags.ABSTRACT,
     Properties: {
         'gesture-modes': GObject.ParamSpec.flags(
@@ -283,8 +284,9 @@ var BaseAppView = GObject.registerClass({
         this._box.add_child(this._pageIndicators);
 
         // Swipe
+        // FIXME
         this._swipeTracker = new SwipeTracker.SwipeTracker(this._scrollView,
-            Clutter.Orientation.HORIZONTAL, this.gestureModes);
+            Clutter.Orientation.HORIZONTAL);
         this._swipeTracker.orientation = Clutter.Orientation.HORIZONTAL;
         this._swipeTracker.connect('begin', this._swipeBegin.bind(this));
         this._swipeTracker.connect('update', this._swipeUpdate.bind(this));
@@ -395,7 +397,7 @@ var BaseAppView = GObject.registerClass({
             if (hOffset === 0 && vOffset === 0)
                 return;
         }
-
+        // @ts-expect-error
         this._scrollView.update_fade_effect(
             new Clutter.Margin({
                 left: hOffset,
@@ -725,7 +727,7 @@ var BaseAppView = GObject.registerClass({
         this._swipeTracker.enabled = true;
     }
 
-    _onDragCancelled() {
+    _onDragCancelled(overview, source) {
         // At this point, the positions aren't stored yet, thus _redisplay()
         // will move all items to their original positions
         this._redisplay();
@@ -832,6 +834,13 @@ var BaseAppView = GObject.registerClass({
         return [page, position];
     }
 
+    /**
+     * @returns {AppViewItem["prototype"][]}
+     */
+    _loadApps() {
+        throw new GObject.NotImplementedError(`_loadApps in ${this.constructor.name}`);
+    }
+
     _redisplay() {
         let oldApps = this._orderedItems.slice();
         let oldAppIds = oldApps.map(icon => icon.id);
@@ -1348,7 +1357,7 @@ var BaseAppView = GObject.registerClass({
     }
 });
 
-var PageManager = GObject.registerClass({
+export const PageManager = GObject.registerClass({
     Signals: { 'layout-changed': {} },
 }, class PageManager extends GObject.Object {
     _init() {
@@ -1362,8 +1371,10 @@ var PageManager = GObject.registerClass({
     }
 
     _loadPages() {
+        /** @type {GLib.Variant<'aa{sv}'>} */
         const layout = global.settings.get_value('app-picker-layout');
-        this._pages = layout.recursiveUnpack();
+        /** @type { { [key: string]: { [key: string]: number } }[] } */
+        this._pages = (layout.recursiveUnpack());
         if (!this._updatingPages)
             this.emit('layout-changed');
     }
@@ -1385,11 +1396,16 @@ var PageManager = GObject.registerClass({
         return [page, position];
     }
 
-    set pages(p) {
+    /**
+     * @param {{ [key: string]: { [key: string]: GLib.Variant<'i'> } }[]} p
+     */
+    updatePages(p) {
+        /** @type {{ [key: string]: GLib.Variant<'a{sv}'> }[]} */
         const packedPages = [];
 
         // Pack the icon properties as a GVariant
         for (const page of p) {
+            /** @type {{ [key: string]: GLib.Variant<'a{sv}'> }} */
             const pageData = {};
             for (const [appId, properties] of Object.entries(page))
                 pageData[appId] = new GLib.Variant('a{sv}', properties);
@@ -1409,7 +1425,7 @@ var PageManager = GObject.registerClass({
     }
 });
 
-var AppDisplay = GObject.registerClass(
+export const AppDisplay = GObject.registerClass(
 class AppDisplay extends BaseAppView {
     _init() {
         super._init({
@@ -1493,11 +1509,13 @@ class AppDisplay extends BaseAppView {
     }
 
     _savePages() {
+        /** @type {{ [key: string]: { [key: string]: GLib.Variant<'i'> } }[]} */
         const pages = [];
 
         for (let i = 0; i < this._grid.nPages; i++) {
             const pageItems =
                 this._grid.getItemsAtPage(i).filter(c => c.visible);
+            /** @type {{ [key: string]: { [key: string]: GLib.Variant<'i'> } }} */
             const pageData = {};
 
             pageItems.forEach((item, index) => {
@@ -1508,7 +1526,7 @@ class AppDisplay extends BaseAppView {
             pages.push(pageData);
         }
 
-        this._pageManager.pages = pages;
+        this._pageManager.updatePages(pages);
     }
 
     _ensurePlaceholder(source) {
@@ -1752,8 +1770,8 @@ class AppDisplay extends BaseAppView {
         super._maybeMoveItem(clonedEvent);
     }
 
-    _onDragBegin(overview, source) {
-        super._onDragBegin(overview, source);
+    _onDragBegin(_overview, source) {
+        super._onDragBegin();
 
         // When dragging from a folder dialog, the dragged app icon doesn't
         // exist in AppDisplay. We work around that by adding a placeholder
@@ -1778,7 +1796,7 @@ class AppDisplay extends BaseAppView {
     }
 
     _onDragCancelled(overview, source) {
-        const view = _getViewFromIcon(source);
+        const view = source._getView();
 
         if (view instanceof FolderView)
             return;
@@ -1792,7 +1810,7 @@ class AppDisplay extends BaseAppView {
 
         this._savePages();
 
-        let view = _getViewFromIcon(source);
+        let view = source._getView();
         if (view instanceof FolderView)
             view.removeApp(source.app);
 
@@ -1860,7 +1878,7 @@ class AppDisplay extends BaseAppView {
     }
 });
 
-var AppSearchProvider = class AppSearchProvider {
+export class AppSearchProvider {
     constructor() {
         this._appSys = Shell.AppSystem.get_default();
         this.id = 'applications';
@@ -1953,17 +1971,38 @@ var AppSearchProvider = class AppSearchProvider {
     }
 };
 
-var AppViewItem = GObject.registerClass(
+/** 
+ * @typedef {object} ViewParams
+ * @property {boolean} [isDraggable]
+ * @property {boolean} [expandTitleOnHover]
+ */
+
+export const AppViewItem = GObject.registerClass(
 class AppViewItem extends St.Button {
-    _init(params = {}, isDraggable = true, expandTitleOnHover = true) {
+    /**
+     * @param {Partial<St.Button.ConstructorProperties> & ViewParams} [params] 
+     * @param {...any} _args
+     */
+    _init(params = {}, ..._args) {
+        const {
+            isDraggable = true,
+            expandTitleOnHover = true,
+            ...buttonParams
+        } = params;
+
         super._init({
             pivot_point: new Graphene.Point({ x: 0.5, y: 0.5 }),
             reactive: true,
             button_mask: St.ButtonMask.ONE | St.ButtonMask.TWO,
             can_focus: true,
-            ...params,
+            ...buttonParams,
         });
 
+        this._name = '';
+        this._id = '';
+        /** @type {IconGrid.BaseIcon["prototype"] | null} */
+        this.icon = null;
+
         this._delegate = this;
 
         if (isDraggable) {
@@ -2162,7 +2201,7 @@ class AppViewItem extends St.Button {
     }
 });
 
-var FolderGrid = GObject.registerClass(
+export const FolderGrid = GObject.registerClass(
 class FolderGrid extends IconGrid.IconGrid {
     _init() {
         super._init({
@@ -2179,7 +2218,14 @@ class FolderGrid extends IconGrid.IconGrid {
     }
 });
 
-var FolderView = GObject.registerClass(
+/**
+ * @typedef {object} FolderViewParams
+ * @property {import('gi://Gio').Settings} folder
+ * @property {string} id
+ * @property {*} parentView
+ */ 
+
+export const FolderView = GObject.registerClass(
 class FolderView extends BaseAppView {
     _init(folder, id, parentView) {
         super._init({
@@ -2411,7 +2457,7 @@ class FolderView extends BaseAppView {
     }
 });
 
-var FolderIcon = GObject.registerClass({
+export const FolderIcon = GObject.registerClass({
     Signals: {
         'apps-changed': {},
     },
@@ -2519,7 +2565,7 @@ var FolderIcon = GObject.registerClass({
         if (!(source instanceof AppIcon))
             return false;
 
-        let view = _getViewFromIcon(source);
+        let view = source._getView();
         if (!view || !(view instanceof AppDisplay))
             return false;
 
@@ -2590,7 +2636,7 @@ var FolderIcon = GObject.registerClass({
     }
 });
 
-var AppFolderDialog = GObject.registerClass({
+export const AppFolderDialog = GObject.registerClass({
     Signals: {
         'open-state-changed': { param_types: [GObject.TYPE_BOOLEAN] },
     },
@@ -3079,15 +3125,22 @@ var AppFolderDialog = GObject.registerClass({
     }
 });
 
-var AppIcon = GObject.registerClass({
+/** 
+ * @typedef {ViewParams} AppIconParams */
+
+export const AppIcon = GObject.registerClass({
     Signals: {
         'menu-state-changed': { param_types: [GObject.TYPE_BOOLEAN] },
         'sync-tooltip': {},
     },
 }, class AppIcon extends AppViewItem {
-    _init(app, iconParams = {}) {
+    /**
+     * @param {Shell.App} app 
+     * @param {AppIconParams} params 
+     */
+    _init(app, params = {}) {
         // Get the isDraggable property without passing it on to the BaseIcon:
-        const { isDraggable = true, expandTitleOnHover } = iconParams;
+        const { isDraggable = true, expandTitleOnHover } = params;
 
         super._init({ style_class: 'app-well-app' }, isDraggable, expandTitleOnHover);
 
@@ -3102,8 +3155,11 @@ var AppIcon = GObject.registerClass({
 
         this._folderPreviewId = 0;
 
-        iconParams['createIcon'] = this._createIcon.bind(this);
-        iconParams['setSizeManually'] = true;
+        const iconParams = {
+            ...params,
+            createIcon: this._createIcon.bind(this),
+            setSizeManually: true,
+        };
         this.icon = new IconGrid.BaseIcon(app.get_name(), iconParams);
         this._iconContainer.add_child(this.icon);
 
@@ -3292,6 +3348,15 @@ var AppIcon = GObject.registerClass({
         this.icon.animateZoomOutAtPos(x, y);
     }
 
+    /** 
+     * @typedef {object} WorkspaceLaunchParams
+     * @property {number} workspace
+     * @property {number} timestamp
+     */
+    
+    /**
+     * @param {Partial<WorkspaceLaunchParams>} params
+     */
     shellWorkspaceLaunch(params = {}) {
         let { stack } = new Error();
         log('shellWorkspaceLaunch is deprecated, use app.open_new_window() instead\n%s'.format(stack));
@@ -3332,7 +3397,7 @@ var AppIcon = GObject.registerClass({
     }
 
     _canAccept(source) {
-        let view = _getViewFromIcon(source);
+        let view = source._getView();
 
         return source != this &&
                (source instanceof this.constructor) &&
@@ -3366,12 +3431,20 @@ var AppIcon = GObject.registerClass({
         }
     }
 
+    _getView() {
+        for (let parent = this.get_parent(); parent; parent = parent.get_parent()) {
+            if (parent instanceof BaseAppView)
+                return parent;
+        }
+        return null;
+    }
+
     acceptDrop(source, actor, x) {
         const accepted = super.acceptDrop(source, actor, x);
         if (!accepted)
             return false;
 
-        let view = _getViewFromIcon(this);
+        let view = this._getView();
         let apps = [this.id, source.id];
 
         return view?.createFolder(apps);
diff --git a/js/ui/appFavorites.js b/js/ui/appFavorites.js
index 67e74e7992..d9f45afbc2 100644
--- a/js/ui/appFavorites.js
+++ b/js/ui/appFavorites.js
@@ -1,11 +1,11 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported getAppFavorites */
 
-const Shell = imports.gi.Shell;
-const ParentalControlsManager = imports.misc.parentalControlsManager;
-const Signals = imports.misc.signals;
+import Shell from 'gi://Shell';
+import * as ParentalControlsManager from '../misc/parentalControlsManager.js';
+import * as Signals from '../misc/signals.js';
 
-const Main = imports.ui.main;
+import Main from './main.js';
 
 // In alphabetical order
 const RENAMED_DESKTOP_IDS = {
@@ -204,8 +204,8 @@ class AppFavorites extends Signals.EventEmitter {
     }
 }
 
-var appFavoritesInstance = null;
-function getAppFavorites() {
+let appFavoritesInstance = null;
+export function getAppFavorites() {
     if (appFavoritesInstance == null)
         appFavoritesInstance = new AppFavorites();
     return appFavoritesInstance;
diff --git a/js/ui/appMenu.js b/js/ui/appMenu.js
index 87d2218cdf..0ec94adc2d 100644
--- a/js/ui/appMenu.js
+++ b/js/ui/appMenu.js
@@ -2,12 +2,12 @@
 /* exported AppMenu */
 const { Clutter, Gio, GLib, Meta, Shell, St } = imports.gi;
 
-const AppFavorites = imports.ui.appFavorites;
-const Main = imports.ui.main;
-const ParentalControlsManager = imports.misc.parentalControlsManager;
-const PopupMenu = imports.ui.popupMenu;
+import * as AppFavorites from './appFavorites.js';
+import Main from './main.js';
+import * as ParentalControlsManager from '../misc/parentalControlsManager.js';
+import * as PopupMenu from './popupMenu.js';
 
-var AppMenu = class AppMenu extends PopupMenu.PopupMenu {
+export class AppMenu extends PopupMenu.PopupMenu {
     /**
      * @param {Clutter.Actor} sourceActor - actor the menu is attached to
      * @param {St.Side} side - arrow side
diff --git a/js/ui/audioDeviceSelection.js b/js/ui/audioDeviceSelection.js
index 8660663f92..1371db1ca9 100644
--- a/js/ui/audioDeviceSelection.js
+++ b/js/ui/audioDeviceSelection.js
@@ -1,13 +1,20 @@
 /* exported AudioDeviceSelectionDBus */
-const { Clutter, Gio, GLib, GObject, Meta, Shell, St } = imports.gi;
+import Clutter from 'gi://Clutter';
+import Gio from 'gi://Gio';
+import GLib from 'gi://GLib';
+import GObject from 'gi://GObject';
+import Meta from 'gi://Meta';
+import Shell from 'gi://Shell';
+import St from 'gi://St';
 
-const Dialog = imports.ui.dialog;
-const Main = imports.ui.main;
-const ModalDialog = imports.ui.modalDialog;
 
-const { loadInterfaceXML } = imports.misc.fileUtils;
+import * as Dialog from './dialog.js';
+import Main from './main.js';
+import * as ModalDialog from './modalDialog.js';
 
-var AudioDevice = {
+import { loadInterfaceXML } from '../misc/fileUtilsModule.js';
+
+export const AudioDevice = {
     HEADPHONES: 1 << 0,
     HEADSET:    1 << 1,
     MICROPHONE: 1 << 2,
@@ -15,14 +22,20 @@ var AudioDevice = {
 
 const AudioDeviceSelectionIface = loadInterfaceXML('org.gnome.Shell.AudioDeviceSelection');
 
-var AudioDeviceSelectionDialog = GObject.registerClass({
+export const AudioDeviceSelectionDialog = GObject.registerClass({
     Signals: { 'device-selected': { param_types: [GObject.TYPE_UINT] } },
 }, class AudioDeviceSelectionDialog extends ModalDialog.ModalDialog {
+    /**
+     * @param {*} devices 
+     */
     _init(devices) {
         super._init({ styleClass: 'audio-device-selection-dialog' });
 
         this._deviceItems = {};
 
+        /** @type {string} */
+        this._sender = null;
+
         this._buildLayout();
 
         if (devices & AudioDevice.HEADPHONES)
@@ -135,7 +148,7 @@ var AudioDeviceSelectionDialog = GObject.registerClass({
     }
 });
 
-var AudioDeviceSelectionDBus = class AudioDeviceSelectionDBus {
+export class AudioDeviceSelectionDBus {
     constructor() {
         this._audioSelectionDialog = null;
 
diff --git a/js/ui/background.js b/js/ui/background.js
index f5e7b12913..db6fde567d 100644
--- a/js/ui/background.js
+++ b/js/ui/background.js
@@ -94,15 +94,22 @@
 //     MetaBackgroundImage         MetaBackgroundImage
 //     MetaBackgroundImage         MetaBackgroundImage
 
-const { Clutter, GDesktopEnums, Gio, GLib, GObject, GnomeDesktop, Meta } = imports.gi;
-const Signals = imports.misc.signals;
+import Clutter from 'gi://Clutter';
+import GDesktopEnums from 'gi://GDesktopEnums';
+import Gio from 'gi://Gio';
+import GLib from 'gi://GLib';
+import GObject from 'gi://GObject';
+import GnomeDesktop from 'gi://GnomeDesktop';
+import Meta from 'gi://Meta';
 
-const LoginManager = imports.misc.loginManager;
-const Main = imports.ui.main;
+import * as Signals from '../misc/signals.js';
+
+import * as LoginManager from '../misc/loginManager.js';
+import Main from './main.js';
 
 Gio._promisify(Gio._LocalFilePrototype, 'query_info_async', 'query_info_finish');
 
-var DEFAULT_BACKGROUND_COLOR = Clutter.Color.from_pixel(0x2e3436ff);
+export const DEFAULT_BACKGROUND_COLOR = Clutter.Color.from_pixel(0x2e3436ff);
 
 const BACKGROUND_SCHEMA = 'org.gnome.desktop.background';
 const PRIMARY_COLOR_KEY = 'primary-color';
@@ -111,14 +118,14 @@ const COLOR_SHADING_TYPE_KEY = 'color-shading-type';
 const BACKGROUND_STYLE_KEY = 'picture-options';
 const PICTURE_URI_KEY = 'picture-uri';
 
-var FADE_ANIMATION_TIME = 1000;
+export let FADE_ANIMATION_TIME = 1000;
 
-// These parameters affect how often we redraw.
-// The first is how different (percent crossfaded) the slide show
-// has to look before redrawing and the second is the minimum
-// frequency (in seconds) we're willing to wake up
-var ANIMATION_OPACITY_STEP_INCREMENT = 4.0;
-var ANIMATION_MIN_WAKEUP_INTERVAL = 1.0;
+    // These parameters affect how often we redraw.
+    // The first is how different (percent crossfaded) the slide show
+    // has to look before redrawing and the second is the minimum
+    // frequency (in seconds) we're willing to wake up
+export let ANIMATION_OPACITY_STEP_INCREMENT = 4.0;
+export let ANIMATION_MIN_WAKEUP_INTERVAL = 1.0;
 
 let _backgroundCache = null;
 
@@ -132,12 +139,13 @@ function _fileEqual0(file1, file2) {
     return file1.equal(file2);
 }
 
-var BackgroundCache = class BackgroundCache extends Signals.EventEmitter {
+export class BackgroundCache extends Signals.EventEmitter {
     constructor() {
         super();
 
         this._fileMonitors = {};
         this._backgroundSources = {};
+        /** @type {{ [key: string]: Animation["prototype"] }} */
         this._animations = {};
     }
 
@@ -159,6 +167,16 @@ var BackgroundCache = class BackgroundCache extends Signals.EventEmitter {
         this._fileMonitors[key] = monitor;
     }
 
+    /** 
+     * @typedef {object} AnimationParams
+     * @property {Gio.File} file
+     * @property {string} settingsSchema
+     * @property {((animation: Animation["prototype"]) => void) | null} onLoaded
+     */
+    
+    /**
+     * @param {Partial<AnimationParams>} [params]
+     */
     getAnimation(params = {}) {
         const {
             file = null,
@@ -219,13 +237,13 @@ var BackgroundCache = class BackgroundCache extends Signals.EventEmitter {
     }
 };
 
-function getBackgroundCache() {
+export function getBackgroundCache() {
     if (!_backgroundCache)
         _backgroundCache = new BackgroundCache();
     return _backgroundCache;
 }
 
-var Background = GObject.registerClass({
+export const Background = GObject.registerClass({
     Signals: { 'loaded': {}, 'bg-changed': {} },
 }, class Background extends Meta.Background {
     _init(params = {}) {
@@ -248,6 +266,8 @@ var Background = GObject.registerClass({
         this._cancellable = new Gio.Cancellable();
         this.isLoaded = false;
 
+        this._changedId = -1;
+
         this._clock = new GnomeDesktop.WallClock();
         this._timezoneChangedId = this._clock.connect('notify::timezone',
             () => {
@@ -269,6 +289,17 @@ var Background = GObject.registerClass({
         this._load();
     }
 
+    /**
+     * @param {(background: Background) => void} callback 
+     */
+    onNextChange(callback) {
+        this._changedId = this.connect('bg-changed', (background) => {
+            background.disconnect(this._changedId);
+
+            callback(background);
+        });
+    }
+
     destroy() {
         this._cancellable.cancel();
         this._removeAnimationTimeout();
@@ -515,7 +546,7 @@ var Background = GObject.registerClass({
 
 let _systemBackground;
 
-var SystemBackground = GObject.registerClass({
+export const SystemBackground = GObject.registerClass({
     Signals: { 'loaded': {} },
 }, class SystemBackground extends Meta.BackgroundActor {
     _init() {
@@ -538,7 +569,7 @@ var SystemBackground = GObject.registerClass({
     }
 });
 
-var BackgroundSource = class BackgroundSource {
+export class BackgroundSource {
     constructor(layoutManager, settingsSchema) {
         // Allow override the background image setting for performance testing
         this._layoutManager = layoutManager;
@@ -601,9 +632,9 @@ var BackgroundSource = class BackgroundSource {
                 style,
             });
 
-            background._changedId = background.connect('bg-changed', () => {
-                background.disconnect(background._changedId);
-                background.destroy();
+            background.onNextChange((bg) => {
+                bg.destroy();
+
                 delete this._backgrounds[monitorIndex];
             });
 
@@ -627,8 +658,11 @@ var BackgroundSource = class BackgroundSource {
     }
 };
 
-var Animation = GObject.registerClass(
+export const Animation = GObject.registerClass(
 class Animation extends GnomeDesktop.BGSlideShow {
+    /**
+     * @param {*} params 
+     */
     _init(params) {
         super._init(params);
 
@@ -638,6 +672,16 @@ class Animation extends GnomeDesktop.BGSlideShow {
         this.loaded = false;
     }
 
+    /**
+     * @returns {false}
+     */
+    load() {
+        throw new GObject.NotImplementedError(`Animation.prototype.load is not supported. Use loadAsync.`);
+    }
+
+    /**
+     * @param {() => void} callback 
+     */
     loadAsync(callback) {
         this.load_async(null, () => {
             this.loaded = true;
@@ -666,7 +710,7 @@ class Animation extends GnomeDesktop.BGSlideShow {
     }
 });
 
-var BackgroundManager = class BackgroundManager extends Signals.EventEmitter {
+export class BackgroundManager extends Signals.EventEmitter {
     constructor(params = {}) {
         super();
 
@@ -746,7 +790,7 @@ var BackgroundManager = class BackgroundManager extends Signals.EventEmitter {
 
         const { background } = newBackgroundActor.content;
 
-        if (background.isLoaded) {
+        if (background instanceof Background && background.isLoaded) {
             this._swapBackgroundActor();
         } else {
             newBackgroundActor.loadedSignalId = background.connect('loaded',
diff --git a/js/ui/backgroundMenu.js b/js/ui/backgroundMenu.js
index e31e4c1a99..a311de5592 100644
--- a/js/ui/backgroundMenu.js
+++ b/js/ui/backgroundMenu.js
@@ -1,13 +1,14 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported addBackgroundMenu */
 
-const { Clutter, St } = imports.gi;
+import Clutter from 'gi://Clutter';
+import St from 'gi://St';
 
-const BoxPointer = imports.ui.boxpointer;
-const Main = imports.ui.main;
-const PopupMenu = imports.ui.popupMenu;
+import * as BoxPointer from './boxpointer.js';
+import Main from './main.js';
+import * as PopupMenu from './popupMenu.js';
 
-var BackgroundMenu = class BackgroundMenu extends PopupMenu.PopupMenu {
+export class BackgroundMenu extends PopupMenu.PopupMenu {
     constructor(layoutManager) {
         super(layoutManager.dummyCursor, 0, St.Side.TOP);
 
@@ -23,7 +24,7 @@ var BackgroundMenu = class BackgroundMenu extends PopupMenu.PopupMenu {
     }
 };
 
-function addBackgroundMenu(actor, layoutManager) {
+export function addBackgroundMenu(actor, layoutManager) {
     actor.reactive = true;
     actor._backgroundMenu = new BackgroundMenu(layoutManager);
     actor._backgroundManager = new PopupMenu.PopupMenuManager(actor);
diff --git a/js/ui/barLevel.js b/js/ui/barLevel.js
index 25d4835280..ebf3171ccd 100644
--- a/js/ui/barLevel.js
+++ b/js/ui/barLevel.js
@@ -1,9 +1,13 @@
 /* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
 /* exported BarLevel */
 
-const { Atk, Clutter, GObject, St } = imports.gi;
+import Atk from 'gi://Atk';
+import Clutter from 'gi://Clutter';
+import GObject from 'gi://GObject';
+import St from 'gi://St';
 
-var BarLevel = GObject.registerClass({
+
+export const BarLevel = GObject.registerClass({
     Properties: {
         'value': GObject.ParamSpec.double(
             'value', 'value', 'value',
@@ -19,6 +23,9 @@ var BarLevel = GObject.registerClass({
             1, 2, 1),
     },
 }, class BarLevel extends St.DrawingArea {
+    /**
+     * @param {*} params 
+     */
     _init(params) {
         this._maxValue = 1;
         this._value = 0;
diff --git a/js/ui/boxpointer.js b/js/ui/boxpointer.js
index be9c57ce03..f10ae99339 100644
--- a/js/ui/boxpointer.js
+++ b/js/ui/boxpointer.js
@@ -1,18 +1,23 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported BoxPointer */
 
-const { Clutter, GObject, Meta, St } = imports.gi;
+import Clutter from 'gi://Clutter';
+import GObject from 'gi://GObject';
+import Meta from 'gi://Meta';
+import St from 'gi://St';
 
-const Main = imports.ui.main;
 
-var PopupAnimation = {
+import Main from './main.js';
+
+/** @enum {number} */
+export const PopupAnimation = {
     NONE:  0,
     SLIDE: 1 << 0,
     FADE:  1 << 1,
     FULL:  ~0,
 };
 
-var POPUP_ANIMATION_TIME = 150;
+export let POPUP_ANIMATION_TIME = 150;
 
 /**
  * BoxPointer:
@@ -27,9 +32,13 @@ var POPUP_ANIMATION_TIME = 150;
  * totally inside the monitor workarea if possible.
  *
  */
-var BoxPointer = GObject.registerClass({
+export const BoxPointer = GObject.registerClass({
     Signals: { 'arrow-side-changed': {} },
 }, class BoxPointer extends St.Widget {
+    /**
+     * @param {*} arrowSide 
+     * @param {*} binProperties 
+     */
     _init(arrowSide, binProperties) {
         super._init();
 
@@ -167,6 +176,12 @@ var BoxPointer = GObject.registerClass({
         });
     }
 
+    /**
+     * @param {boolean} isWidth 
+     * @param {number} minSize 
+     * @param {number} natSize
+     * @returns {[number, number]} 
+     */
     _adjustAllocationForArrow(isWidth, minSize, natSize) {
         let themeNode = this.get_theme_node();
         let borderWidth = themeNode.get_length('-arrow-border-width');
@@ -182,6 +197,9 @@ var BoxPointer = GObject.registerClass({
         return [minSize, natSize];
     }
 
+    /**
+     * @returns {[number, number]} 
+     */
     vfunc_get_preferred_width(forHeight) {
         let themeNode = this.get_theme_node();
         forHeight = themeNode.adjust_for_height(forHeight);
diff --git a/js/ui/calendar.js b/js/ui/calendar.js
index b3eb21dc8a..31eef25bff 100644
--- a/js/ui/calendar.js
+++ b/js/ui/calendar.js
@@ -1,23 +1,28 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported Calendar, CalendarMessageList, DBusEventSource */
 
-const { Clutter, Gio, GLib, GObject, Shell, St } = imports.gi;
+import Clutter from 'gi://Clutter';
+import Gio from 'gi://Gio';
+import GLib from 'gi://GLib';
+import GObject from 'gi://GObject';
+import Shell from 'gi://Shell';
+import St from 'gi://St';
 
-const Main = imports.ui.main;
-const MessageList = imports.ui.messageList;
-const MessageTray = imports.ui.messageTray;
-const Mpris = imports.ui.mpris;
-const PopupMenu = imports.ui.popupMenu;
-const Util = imports.misc.util;
+import Main from './main.js';
+import * as MessageList from './messageList.js';
+import * as MessageTray from './messageTray.js';
+import * as Mpris from './mpris.js';
+import * as PopupMenu from './popupMenu.js';
+import * as Util from '../misc/util.js';
 
-const { loadInterfaceXML } = imports.misc.fileUtils;
+import { loadInterfaceXML } from '../misc/fileUtilsModule.js';
 
-var MSECS_IN_DAY = 24 * 60 * 60 * 1000;
-var SHOW_WEEKDATE_KEY = 'show-weekdate';
+const MSECS_IN_DAY = 24 * 60 * 60 * 1000;
+const SHOW_WEEKDATE_KEY = 'show-weekdate';
 
-var MESSAGE_ICON_SIZE = -1; // pick up from CSS
+export let MESSAGE_ICON_SIZE = -1; // pick up from CSS
 
-var NC_ = (context, str) => '%s\u0004%s'.format(context, str);
+export const NC_ = (context, str) => '%s\u0004%s'.format(context, str);
 
 function sameYear(dateA, dateB) {
     return dateA.getYear() == dateB.getYear();
@@ -81,7 +86,7 @@ function _getCalendarDayAbbreviation(dayNumber) {
 
 // Abstraction for an appointment/event in a calendar
 
-var CalendarEvent = class CalendarEvent {
+export class CalendarEvent {
     constructor(id, date, end, summary, allDay) {
         this.id = id;
         this.date = date;
@@ -94,7 +99,7 @@ var CalendarEvent = class CalendarEvent {
 // Interface for appointments/events - e.g. the contents of a calendar
 //
 
-var EventSourceBase = GObject.registerClass({
+export const EventSourceBase = GObject.registerClass({
     GTypeFlags: GObject.TypeFlags.ABSTRACT,
     Properties: {
         'has-calendars': GObject.ParamSpec.boolean(
@@ -108,10 +113,16 @@ var EventSourceBase = GObject.registerClass({
     },
     Signals: { 'changed': {} },
 }, class EventSourceBase extends GObject.Object {
+    /**
+     * @returns {boolean}
+     */
     get isLoading() {
         throw new GObject.NotImplementedError('isLoading in %s'.format(this.constructor.name));
     }
 
+    /**
+     * @returns {boolean}
+     */
     get hasCalendars() {
         throw new GObject.NotImplementedError('hasCalendars in %s'.format(this.constructor.name));
     }
@@ -123,16 +134,22 @@ var EventSourceBase = GObject.registerClass({
         throw new GObject.NotImplementedError('requestRange in %s'.format(this.constructor.name));
     }
 
+    /**
+     * @returns {any[]}
+     */
     getEvents(_begin, _end) {
         throw new GObject.NotImplementedError('getEvents in %s'.format(this.constructor.name));
     }
 
+    /**
+     * @returns {boolean}
+     */
     hasEvents(_day) {
         throw new GObject.NotImplementedError('hasEvents in %s'.format(this.constructor.name));
     }
 });
 
-var EmptyEventSource = GObject.registerClass(
+export const EmptyEventSource = GObject.registerClass(
 class EmptyEventSource extends EventSourceBase {
     get isLoading() {
         return false;
@@ -185,7 +202,7 @@ function _dateIntervalsOverlap(a0, a1, b0, b1) {
 }
 
 // an implementation that reads data from a session bus service
-var DBusEventSource = GObject.registerClass(
+export const DBusEventSource = GObject.registerClass(
 class DBusEventSource extends EventSourceBase {
     _init() {
         super._init();
@@ -373,9 +390,11 @@ class DBusEventSource extends EventSourceBase {
     }
 });
 
-var Calendar = GObject.registerClass({
+export const Calendar = GObject.registerClass({
     Signals: { 'selected-date-changed': { param_types: [GLib.DateTime.$gtype] } },
-}, class Calendar extends St.Widget {
+},
+/** @extends {St.Widget<Clutter.GridLayout>} */
+class Calendar extends St.Widget {
     _init() {
         this._weekStart = Shell.util_get_week_start();
         this._settings = new Gio.Settings({ schema_id: 'org.gnome.desktop.calendar' });
@@ -719,7 +738,7 @@ var Calendar = GObject.registerClass({
     }
 });
 
-var NotificationMessage = GObject.registerClass(
+export const NotificationMessage = GObject.registerClass(
 class NotificationMessage extends MessageList.Message {
     _init(notification) {
         super._init(notification.title, notification.bannerBodyText);
@@ -784,7 +803,7 @@ class NotificationMessage extends MessageList.Message {
     }
 });
 
-var TimeLabel = GObject.registerClass(
+export const TimeLabel = GObject.registerClass(
 class NotificationTimeLabel extends St.Label {
     _init(datetime) {
         super._init({
@@ -801,7 +820,7 @@ class NotificationTimeLabel extends St.Label {
     }
 });
 
-var NotificationSection = GObject.registerClass(
+export const NotificationSection = GObject.registerClass(
 class NotificationSection extends MessageList.MessageListSection {
     _init() {
         super._init();
@@ -882,7 +901,7 @@ class NotificationSection extends MessageList.MessageListSection {
     }
 });
 
-var Placeholder = GObject.registerClass(
+export const Placeholder = GObject.registerClass(
 class Placeholder extends St.BoxLayout {
     _init() {
         super._init({ style_class: 'message-list-placeholder', vertical: true });
@@ -918,7 +937,7 @@ class DoNotDisturbSwitch extends PopupMenu.Switch {
     }
 });
 
-var CalendarMessageList = GObject.registerClass(
+export const CalendarMessageList = GObject.registerClass(
 class CalendarMessageList extends St.Widget {
     _init() {
         super._init({
@@ -982,6 +1001,7 @@ class CalendarMessageList extends St.Widget {
             this._clearButton, 'visible',
             GObject.BindingFlags.INVERT_BOOLEAN);
 
+        /** @type {St.BoxLayout<MessageList.MessageListSection["prototype"]>} */
         this._sectionList = new St.BoxLayout({ style_class: 'message-list-sections',
                                                vertical: true,
                                                x_expand: true,
diff --git a/js/ui/checkBox.js b/js/ui/checkBox.js
index d64bd0d6c2..4e227f6185 100644
--- a/js/ui/checkBox.js
+++ b/js/ui/checkBox.js
@@ -1,7 +1,12 @@
 /* exported CheckBox */
-const { Atk, Clutter, GObject, Pango, St } = imports.gi;
+import Atk from 'gi://Atk';
+import Clutter from 'gi://Clutter';
+import GObject from 'gi://GObject';
+import Pango from 'gi://Pango';
+import St from 'gi://St';
 
-var CheckBox = GObject.registerClass(
+
+export const CheckBox = GObject.registerClass(
 class CheckBox extends St.Button {
     _init(label) {
         let container = new St.BoxLayout({
diff --git a/js/ui/closeDialog.js b/js/ui/closeDialog.js
index 63a0bcfcf8..8b66edf093 100644
--- a/js/ui/closeDialog.js
+++ b/js/ui/closeDialog.js
@@ -1,21 +1,30 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported CloseDialog */
 
-const { Clutter, GLib, GObject, Meta, Shell, St } = imports.gi;
+import Clutter from 'gi://Clutter';
+import GLib from 'gi://GLib';
+import GObject from 'gi://GObject';
+import Meta from 'gi://Meta';
+import Shell from 'gi://Shell';
+import St from 'gi://St';
 
-const Dialog = imports.ui.dialog;
-const Main = imports.ui.main;
 
-var FROZEN_WINDOW_BRIGHTNESS = -0.3;
-var DIALOG_TRANSITION_TIME = 150;
-var ALIVE_TIMEOUT = 5000;
+import * as Dialog from './dialog.js';
+import Main from './main.js';
 
-var CloseDialog = GObject.registerClass({
+export let FROZEN_WINDOW_BRIGHTNESS = -0.3;
+export let DIALOG_TRANSITION_TIME = 150;
+export let ALIVE_TIMEOUT = 5000;
+
+export const CloseDialog = GObject.registerClass({
     Implements: [Meta.CloseDialog],
     Properties: {
         'window': GObject.ParamSpec.override('window', Meta.CloseDialog),
     },
 }, class CloseDialog extends GObject.Object {
+    /**
+     * @param {Meta.Window} window
+     */
     _init(window) {
         super._init();
         this._window = window;
@@ -30,6 +39,9 @@ var CloseDialog = GObject.registerClass({
         return this._window;
     }
 
+    /**
+     * @param {Meta.Window} window
+     */
     set window(window) {
         this._window = window;
     }
@@ -61,6 +73,7 @@ var CloseDialog = GObject.registerClass({
         if (this._dialog)
             return;
 
+        /** @type {Meta.WindowActor} */
         let windowActor = this._window.get_compositor_private();
         this._dialog = new Dialog.Dialog(windowActor, 'close-dialog');
         this._dialog.width = windowActor.width;
@@ -86,6 +99,7 @@ var CloseDialog = GObject.registerClass({
         // We set the effect on the surface actor, so the dialog itself
         // (which is a child of the MetaWindowActor) does not get the
         // effect applied itself.
+        /** @type {Meta.WindowActor} */
         let windowActor = this._window.get_compositor_private();
         let surfaceActor = windowActor.get_first_child();
         let effect = new Clutter.BrightnessContrastEffect();
@@ -94,15 +108,22 @@ var CloseDialog = GObject.registerClass({
     }
 
     _removeWindowEffect() {
+        /** @type {Meta.WindowActor} */
         let windowActor = this._window.get_compositor_private();
         let surfaceActor = windowActor.get_first_child();
         surfaceActor.remove_effect_by_name("gnome-shell-frozen-window");
     }
 
+    /**
+     * @this {Meta.CloseDialog}
+     */
     _onWait() {
         this.response(Meta.CloseDialogResponse.WAIT);
     }
 
+    /**
+     * @this {Meta.CloseDialog}
+     */
     _onClose() {
         this.response(Meta.CloseDialogResponse.FORCE_CLOSE);
     }
diff --git a/js/ui/components/__init__.js b/js/ui/components.js
similarity index 70%
rename from js/ui/components/__init__.js
rename to js/ui/components.js
index 74300136b7..db41db370a 100644
--- a/js/ui/components/__init__.js
+++ b/js/ui/components.js
@@ -1,7 +1,7 @@
 /* exported ComponentManager */
-const Main = imports.ui.main;
+import Main from './main.js';
 
-var ComponentManager = class {
+export class ComponentManager {
     constructor() {
         this._allComponents = {};
         this._enabledComponents = [];
@@ -10,12 +10,12 @@ var ComponentManager = class {
         this._sessionUpdated();
     }
 
-    _sessionUpdated() {
+    async _sessionUpdated() {
         let newEnabledComponents = Main.sessionMode.components;
 
-        newEnabledComponents
+        await Promise.all([...newEnabledComponents
             .filter(name => !this._enabledComponents.includes(name))
-            .forEach(name => this._enableComponent(name));
+            .map(name => this._enableComponent(name))]);
 
         this._enabledComponents
             .filter(name => !newEnabledComponents.includes(name))
@@ -24,12 +24,12 @@ var ComponentManager = class {
         this._enabledComponents = newEnabledComponents;
     }
 
-    _importComponent(name) {
-        let module = imports.ui.components[name];
+    async _importComponent(name) {
+        let module = await import(`./components/${name}.js`);
         return module.Component;
     }
 
-    _ensureComponent(name) {
+    async _ensureComponent(name) {
         let component = this._allComponents[name];
         if (component)
             return component;
@@ -37,14 +37,14 @@ var ComponentManager = class {
         if (Main.sessionMode.isLocked)
             return null;
 
-        let constructor = this._importComponent(name);
+        let constructor = await this._importComponent(name);
         component = new constructor();
         this._allComponents[name] = component;
         return component;
     }
 
-    _enableComponent(name) {
-        let component = this._ensureComponent(name);
+    async _enableComponent(name) {
+        let component = await this._ensureComponent(name);
         if (component)
             component.enable();
     }
diff --git a/js/ui/components/automountManager.js b/js/ui/components/automountManager.js
index ecf9fa144c..7cd2735012 100644
--- a/js/ui/components/automountManager.js
+++ b/js/ui/components/automountManager.js
@@ -1,25 +1,26 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported Component */
 
-const { Gio, GLib } = imports.gi;
+import Gio from 'gi://Gio';
+import GLib from 'gi://GLib';
 
-const GnomeSession = imports.misc.gnomeSession;
-const Main = imports.ui.main;
-const ShellMountOperation = imports.ui.shellMountOperation;
+import * as GnomeSession from '../../misc/gnomeSession.js';
+import Main from './../main.js';
+import * as ShellMountOperation from './../shellMountOperation.js';
 
-var GNOME_SESSION_AUTOMOUNT_INHIBIT = 16;
+export let GNOME_SESSION_AUTOMOUNT_INHIBIT = 16;
 
 // GSettings keys
 const SETTINGS_SCHEMA = 'org.gnome.desktop.media-handling';
 const SETTING_ENABLE_AUTOMOUNT = 'automount';
 
-var AUTORUN_EXPIRE_TIMEOUT_SECS = 10;
+export let AUTORUN_EXPIRE_TIMEOUT_SECS = 10;
 
-var AutomountManager = class {
+export class AutomountManager {
     constructor() {
         this._settings = new Gio.Settings({ schema_id: SETTINGS_SCHEMA });
         this._activeOperations = new Map();
-        this._session = new GnomeSession.SessionManager();
+        this._session = GnomeSession.SessionManager();
         this._session.connectSignal('InhibitorAdded',
                                     this._InhibitorsChanged.bind(this));
         this._session.connectSignal('InhibitorRemoved',
@@ -130,6 +131,18 @@ var AutomountManager = class {
         this._checkAndMountVolume(volume);
     }
 
+    /** 
+     * @typedef {object} VolumeMountProps: An object with the properties:
+     * @property {boolean} [checkSession]
+     * @property {boolean} [useMountOp]
+     * @property {boolean} [allowAutorun]
+     */    
+
+    /**
+     * _checkAndMountVolume:
+     * @param {Gio.Volume} volume
+     * @param {Partial<VolumeMountProps>} [params]
+     */
     _checkAndMountVolume(volume, params = {}) {
         const {
             checkSession = true,
@@ -253,4 +266,4 @@ var AutomountManager = class {
         GLib.Source.set_name_by_id(id, '[gnome-shell] volume.allowAutorun');
     }
 };
-var Component = AutomountManager;
+export let Component = AutomountManager;
diff --git a/js/ui/components/autorunManager.js b/js/ui/components/autorunManager.js
index 16b52cfe3e..6b321f3e9a 100644
--- a/js/ui/components/autorunManager.js
+++ b/js/ui/components/autorunManager.js
@@ -1,13 +1,17 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported Component */
 
-const { Clutter, Gio, GObject, St } = imports.gi;
+import Clutter from 'gi://Clutter';
+import Gio from 'gi://Gio';
+import GObject from 'gi://GObject';
+import St from 'gi://St';
 
-const GnomeSession = imports.misc.gnomeSession;
-const Main = imports.ui.main;
-const MessageTray = imports.ui.messageTray;
 
-const { loadInterfaceXML } = imports.misc.fileUtils;
+import * as GnomeSession from '../../misc/gnomeSession.js';
+import Main from './../main.js';
+import * as MessageTray from './../messageTray.js';
+
+import { loadInterfaceXML } from '../../misc/fileUtilsModule.js';
 
 // GSettings keys
 const SETTINGS_SCHEMA = 'org.gnome.desktop.media-handling';
@@ -16,7 +20,8 @@ const SETTING_START_APP = 'autorun-x-content-start-app';
 const SETTING_IGNORE = 'autorun-x-content-ignore';
 const SETTING_OPEN_FOLDER = 'autorun-x-content-open-folder';
 
-var AutorunSetting = {
+/** @enum {number} */
+export const AutorunSetting = {
     RUN: 0,
     IGNORE: 1,
     FILES: 2,
@@ -80,7 +85,7 @@ function HotplugSniffer() {
                                '/org/gnome/Shell/HotplugSniffer');
 }
 
-var ContentTypeDiscoverer = class {
+export class ContentTypeDiscoverer {
     constructor(callback) {
         this._callback = callback;
         this._settings = new Gio.Settings({ schema_id: SETTINGS_SCHEMA });
@@ -142,9 +147,9 @@ var ContentTypeDiscoverer = class {
     }
 };
 
-var AutorunManager = class {
+export class AutorunManager {
     constructor() {
-        this._session = new GnomeSession.SessionManager();
+        this._session = GnomeSession.SessionManager();
         this._volumeMonitor = Gio.VolumeMonitor.get();
 
         this._dispatcher = new AutorunDispatcher(this);
@@ -160,7 +165,10 @@ var AutorunManager = class {
         this._volumeMonitor.disconnect(this._mountRemovedId);
     }
 
-    _onMountAdded(monitor, mount) {
+    /**
+     * @param {Gio.Mount} mount 
+     */
+    _onMountAdded(_, mount) {
         // don't do anything if our session is not the currently
         // active one
         if (!this._session.SessionIsActive)
@@ -177,7 +185,10 @@ var AutorunManager = class {
     }
 };
 
-var AutorunDispatcher = class {
+export class AutorunDispatcher {
+    /**
+     * @param {AutorunManager} manager 
+     */
     constructor(manager) {
         this._manager = manager;
         this._sources = [];
@@ -212,6 +223,10 @@ var AutorunDispatcher = class {
         return null;
     }
 
+    /**
+     * @param {Gio.Mount} mount
+     * @param {Gio.Application[]} apps
+     */
     _addSource(mount, apps) {
         // if we already have a source showing for this
         // mount, return
@@ -222,6 +237,10 @@ var AutorunDispatcher = class {
         this._sources.push(new AutorunSource(this._manager, mount, apps));
     }
 
+    /**
+     * @param {Gio.Mount} mount
+     * @param {Gio.Application[]} apps
+     */
     addMount(mount, apps, contentTypes) {
         // if autorun is disabled globally, return
         if (this._settings.get_boolean(SETTING_DISABLE_AUTORUN))
@@ -271,7 +290,14 @@ var AutorunDispatcher = class {
     }
 };
 
-var AutorunSource = GObject.registerClass(
+/**
+ * @typedef {object} AutorunSourceParams
+ * @property {AutorunManager} manager
+ * @property {Gio.Mount} mount
+ * @property {Gio.Application[]} apps
+ */
+
+export const AutorunSource = GObject.registerClass(
 class AutorunSource extends MessageTray.Source {
     _init(manager, mount, apps) {
         super._init(mount.get_name());
@@ -296,8 +322,12 @@ class AutorunSource extends MessageTray.Source {
     }
 });
 
-var AutorunNotification = GObject.registerClass(
+export const AutorunNotification = GObject.registerClass(
 class AutorunNotification extends MessageTray.Notification {
+    /**
+     * @param {*} manager 
+     * @param {*} source 
+     */
     _init(manager, source) {
         super._init(source, source.title);
 
@@ -356,4 +386,4 @@ class AutorunNotification extends MessageTray.Notification {
     }
 });
 
-var Component = AutorunManager;
+export let Component = AutorunManager;
diff --git a/js/ui/components/keyring.js b/js/ui/components/keyring.js
index cd7a81e957..08f17a5fa9 100644
--- a/js/ui/components/keyring.js
+++ b/js/ui/components/keyring.js
@@ -1,15 +1,22 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported Component */
 
-const { Clutter, Gcr, Gio, GObject, Pango, Shell, St } = imports.gi;
-
-const Dialog = imports.ui.dialog;
-const ModalDialog = imports.ui.modalDialog;
-const ShellEntry = imports.ui.shellEntry;
-const CheckBox = imports.ui.checkBox;
-const Util = imports.misc.util;
-
-var KeyringDialog = GObject.registerClass(
+import Clutter from 'gi://Clutter';
+import Gcr from 'gi://Gcr';
+import Gio from 'gi://Gio';
+import GObject from 'gi://GObject';
+import Pango from 'gi://Pango';
+import Shell from 'gi://Shell';
+import St from 'gi://St';
+
+
+import * as Dialog from './../dialog.js';
+import * as ModalDialog from './../modalDialog.js';
+import * as ShellEntry from './../shellEntry.js';
+import * as CheckBox from './../checkBox.js';
+import * as Util from '../../misc/util.js';
+
+export const KeyringDialog = GObject.registerClass(
 class KeyringDialog extends ModalDialog.ModalDialog {
     _init() {
         super._init({ styleClass: 'prompt-dialog' });
@@ -178,7 +185,7 @@ class KeyringDialog extends ModalDialog.ModalDialog {
     }
 });
 
-var KeyringDummyDialog = class {
+export class KeyringDummyDialog {
     constructor() {
         this.prompt = new Shell.KeyringPrompt();
         this.prompt.connect('show-password', this._cancelPrompt.bind(this));
@@ -190,7 +197,7 @@ var KeyringDummyDialog = class {
     }
 };
 
-var KeyringPrompter = GObject.registerClass(
+export const KeyringPrompter = GObject.registerClass(
 class KeyringPrompter extends Gcr.SystemPrompter {
     _init() {
         super._init();
@@ -226,4 +233,4 @@ class KeyringPrompter extends Gcr.SystemPrompter {
     }
 });
 
-var Component = KeyringPrompter;
+export let Component = KeyringPrompter;
diff --git a/js/ui/components/networkAgent.js b/js/ui/components/networkAgent.js
index 367deb666f..5c50004758 100644
--- a/js/ui/components/networkAgent.js
+++ b/js/ui/components/networkAgent.js
@@ -1,14 +1,21 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported Component */
 
-const { Clutter, Gio, GLib, GObject, NM, Pango, Shell, St } = imports.gi;
-const Signals = imports.misc.signals;
-
-const Dialog = imports.ui.dialog;
-const Main = imports.ui.main;
-const MessageTray = imports.ui.messageTray;
-const ModalDialog = imports.ui.modalDialog;
-const ShellEntry = imports.ui.shellEntry;
+import Clutter from 'gi://Clutter';
+import Gio from 'gi://Gio';
+import GLib from 'gi://GLib';
+import GObject from 'gi://GObject';
+import NM from 'gi://NM';
+import Pango from 'gi://Pango';
+import Shell from 'gi://Shell';
+import St from 'gi://St';
+import * as Signals from '../../misc/signals.js';
+
+import * as Dialog from './../dialog.js';
+import Main from './../main.js';
+import * as MessageTray from './../messageTray.js';
+import * as ModalDialog from './../modalDialog.js';
+import * as ShellEntry from './../shellEntry.js';
 
 Gio._promisify(Shell.NetworkAgent.prototype, 'init_async', 'init_finish');
 Gio._promisify(Shell.NetworkAgent.prototype,
@@ -16,11 +23,21 @@ Gio._promisify(Shell.NetworkAgent.prototype,
 
 const VPN_UI_GROUP = 'VPN Plugin UI';
 
-var NetworkSecretDialog = GObject.registerClass(
+export const NetworkSecretDialog = GObject.registerClass(
 class NetworkSecretDialog extends ModalDialog.ModalDialog {
+    /**
+     * @param {any | Shell.NetworkAgent} agent 
+     * @param {string} requestId 
+     * @param {NM.Connection} connection 
+     * @param {string} settingName 
+     * @param {string[]} hints 
+     * @param {number} flags 
+     * @param {{ title: string, message: string, secrets: string[] }} [contentOverride]
+     */
     _init(agent, requestId, connection, settingName, hints, flags, contentOverride) {
         super._init({ styleClass: 'prompt-dialog' });
 
+        /** @type {Shell.NetworkAgent} */
         this._agent = agent;
         this._requestId = requestId;
         this._connection = connection;
@@ -213,7 +230,7 @@ class NetworkSecretDialog extends ModalDialog.ModalDialog {
         case 'none': // static WEP
             secrets.push({
                 label: _('Key'),
-                key: 'wep-key%s'.format(wirelessSecuritySetting.wep_tx_keyidx),
+                key: 'wep-key%s'.format(wirelessSecuritySetting.wep_tx_keyidx.toFixed(0)),
                 value: wirelessSecuritySetting.get_wep_key(wirelessSecuritySetting.wep_tx_keyidx) || '',
                 wep_key_type: wirelessSecuritySetting.wep_key_type,
                 validate: this._validateStaticWep,
@@ -298,7 +315,8 @@ class NetworkSecretDialog extends ModalDialog.ModalDialog {
         else
             setting = this._connection.get_setting_by_name(connectionType);
         secrets.push({ label: _('Password'), key: 'password',
-                       value: setting.value || '', password: true });
+        // TODO: Does not exist: setting.value on NM.Setting
+                       value: '', password: true });
     }
 
     _getContent() {
@@ -354,7 +372,7 @@ class NetworkSecretDialog extends ModalDialog.ModalDialog {
     }
 });
 
-var VPNRequestHandler = class extends Signals.EventEmitter {
+export class VPNRequestHandler extends Signals.EventEmitter {
     constructor(agent, requestId, authHelper, serviceType, connection, hints, flags) {
         super();
 
@@ -527,7 +545,6 @@ var VPNRequestHandler = class extends Signals.EventEmitter {
         let keyfile = new GLib.KeyFile();
         let data;
         let contentOverride;
-
         try {
             data = new GLib.Bytes(this._dataStdout.peek_buffer());
             keyfile.load_from_bytes(data, GLib.KeyFileFlags.NONE);
@@ -563,7 +580,7 @@ var VPNRequestHandler = class extends Signals.EventEmitter {
             }
         } catch (e) {
             // No output is a valid case it means "both secrets are stored"
-            if (data.length > 0) {
+            if (data.get_size() > 0) {
                 logError(e, 'error while reading VPN plugin output keyfile');
 
                 this._agent.respond(this._requestId, Shell.NetworkAgentResponse.INTERNAL_ERROR);
@@ -604,7 +621,7 @@ var VPNRequestHandler = class extends Signals.EventEmitter {
     }
 };
 
-var NetworkAgent = class {
+export class NetworkAgent {
     constructor() {
         this._native = new Shell.NetworkAgent({
             identifier: 'org.gnome.Shell.NetworkAgent',
@@ -807,4 +824,4 @@ var NetworkAgent = class {
         };
     }
 };
-var Component = NetworkAgent;
+export let Component = NetworkAgent;
diff --git a/js/ui/components/polkitAgent.js b/js/ui/components/polkitAgent.js
index 5127ab924d..ad71206789 100644
--- a/js/ui/components/polkitAgent.js
+++ b/js/ui/components/polkitAgent.js
@@ -1,16 +1,25 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported Component */
 
-const { AccountsService, Clutter, GLib,
-        GObject, Pango, PolkitAgent, Polkit, Shell, St } = imports.gi;
-
-const Dialog = imports.ui.dialog;
-const Main = imports.ui.main;
-const ModalDialog = imports.ui.modalDialog;
-const ShellEntry = imports.ui.shellEntry;
-const UserWidget = imports.ui.userWidget;
-const Util = imports.misc.util;
-
+import AccountsService from 'gi://AccountsService';
+import Clutter from 'gi://Clutter';
+import GLib from 'gi://GLib';
+import GObject from 'gi://GObject';
+import Pango from 'gi://Pango';
+import PolkitAgent from 'gi://PolkitAgent';
+import Polkit from 'gi://Polkit';
+import Shell from 'gi://Shell';
+import St from 'gi://St';
+
+
+import * as Dialog from './../dialog.js';
+import Main from './../main.js';
+import * as ModalDialog from './../modalDialog.js';
+import * as ShellEntry from './../shellEntry.js';
+import * as UserWidget from './../userWidget.js';
+import * as Util from '../../misc/util.js';
+
+/** @enum {number} */
 const DialogMode = {
     AUTH: 0,
     CONFIRM: 1,
@@ -20,9 +29,15 @@ const DIALOG_ICON_SIZE = 64;
 
 const DELAYED_RESET_TIMEOUT = 200;
 
-var AuthenticationDialog = GObject.registerClass({
+export const AuthenticationDialog = GObject.registerClass({
     Signals: { 'done': { param_types: [GObject.TYPE_BOOLEAN] } },
 }, class AuthenticationDialog extends ModalDialog.ModalDialog {
+    /**
+     * @param {*} actionId 
+     * @param {*} description 
+     * @param {*} cookie 
+     * @param {*} userNames 
+     */
     _init(actionId, description, cookie, userNames) {
         super._init({ styleClass: 'prompt-dialog' });
 
@@ -410,7 +425,7 @@ var AuthenticationDialog = GObject.registerClass({
     }
 });
 
-var AuthenticationAgent = GObject.registerClass(
+export const AuthenticationAgent = GObject.registerClass(
 class AuthenticationAgent extends Shell.PolkitAuthenticationAgent {
     _init() {
         super._init();
@@ -473,4 +488,4 @@ class AuthenticationAgent extends Shell.PolkitAuthenticationAgent {
     }
 });
 
-var Component = AuthenticationAgent;
+export let Component = AuthenticationAgent;
diff --git a/js/ui/components/telepathyClient.js b/js/ui/components/telepathyClient.js
index 3d7020041c..fc8fb31b62 100644
--- a/js/ui/components/telepathyClient.js
+++ b/js/ui/components/telepathyClient.js
@@ -1,12 +1,29 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported Component */
 
-const { Clutter, Gio, GLib, GObject, St } = imports.gi;
+import Clutter from 'gi://Clutter';
+import Gio from 'gi://Gio';
+import GLib from 'gi://GLib';
+import GObject from 'gi://GObject';
+import St from 'gi://St';
+
+import gi from 'gi';
+
+import Main from './../main.js';
+
+import * as History from '../../misc/history.js';
+import * as MessageList from './../messageList.js';
+import * as MessageTray from './../messageTray.js';
+import * as Util from '../../misc/util.js';
+
+/** @type {import('telepathylogger0')} */
+let Tpl = null;
+/** @type {import('telepathyglib0')} */
+let Tp = null;
 
-var Tpl = null;
-var Tp = null;
 try {
-    ({ TelepathyGLib: Tp, TelepathyLogger: Tpl } = imports.gi);
+    Tp = gi.require('TelepathyGlib');
+    Tpl = gi.require('TelepathyLogger');
 
     Gio._promisify(Tp.Channel.prototype, 'close_async', 'close_finish');
     Gio._promisify(Tp.TextChannel.prototype,
@@ -19,29 +36,23 @@ try {
     log('Telepathy is not available, chat integration will be disabled.');
 }
 
-const History = imports.misc.history;
-const Main = imports.ui.main;
-const MessageList = imports.ui.messageList;
-const MessageTray = imports.ui.messageTray;
-const Util = imports.misc.util;
-
 const HAVE_TP = Tp != null && Tpl != null;
 
 // See Notification.appendMessage
-var SCROLLBACK_IMMEDIATE_TIME = 3 * 60; // 3 minutes
-var SCROLLBACK_RECENT_TIME = 15 * 60; // 15 minutes
-var SCROLLBACK_RECENT_LENGTH = 20;
-var SCROLLBACK_IDLE_LENGTH = 5;
+export let SCROLLBACK_IMMEDIATE_TIME = 3 * 60; // 3 minutes
+export let SCROLLBACK_RECENT_TIME = 15 * 60; // 15 minutes
+export let SCROLLBACK_RECENT_LENGTH = 20;
+export let SCROLLBACK_IDLE_LENGTH = 5;
 
 // See Source._displayPendingMessages
-var SCROLLBACK_HISTORY_LINES = 10;
+export let SCROLLBACK_HISTORY_LINES = 10;
 
 // See Notification._onEntryChanged
-var COMPOSING_STOP_TIMEOUT = 5;
+export let COMPOSING_STOP_TIMEOUT = 5;
 
-var CHAT_EXPAND_LINES = 12;
+export let CHAT_EXPAND_LINES = 12;
 
-var NotificationDirection = {
+export const NotificationDirection = {
     SENT: 'chat-sent',
     RECEIVED: 'chat-received',
 };
@@ -51,8 +62,8 @@ const ChatMessage = HAVE_TP ? GObject.registerClass({
         'message-type': GObject.ParamSpec.int(
             'message-type', 'message-type', 'message-type',
             GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY,
-            Math.min(...Object.values(Tp.ChannelTextMessageType)),
-            Math.max(...Object.values(Tp.ChannelTextMessageType)),
+            Tp.ChannelTextMessageType.NORMAL,
+            Tp.ChannelTextMessageType.DELIVERY_REPORT,
             Tp.ChannelTextMessageType.NORMAL),
         'text': GObject.ParamSpec.string(
             'text', 'text', 'text',
@@ -71,35 +82,52 @@ const ChatMessage = HAVE_TP ? GObject.registerClass({
             GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY,
             null),
     },
-}, class ChatMessageClass extends GObject.Object {
-    static newFromTpMessage(tpMessage, direction) {
-        return new ChatMessage({
-            'message-type': tpMessage.get_message_type(),
-            'text': tpMessage.to_text()[0],
-            'sender': tpMessage.sender.alias,
-            'timestamp': direction === NotificationDirection.RECEIVED
-                ? tpMessage.get_received_timestamp() : tpMessage.get_sent_timestamp(),
-            direction,
-        });
-    }
-
-    static newFromTplTextEvent(tplTextEvent) {
-        let direction =
-            tplTextEvent.get_sender().get_entity_type() === Tpl.EntityType.SELF
-                ? NotificationDirection.SENT : NotificationDirection.RECEIVED;
-
-        return new ChatMessage({
-            'message-type': tplTextEvent.get_message_type(),
-            'text': tplTextEvent.get_message(),
-            'sender': tplTextEvent.get_sender().get_alias(),
-            'timestamp': tplTextEvent.get_timestamp(),
-            direction,
-        });
+}, class ChatMessage extends GObject.Object {
+    /**
+     * @param {{ [key: string]: any }} properties 
+     */
+    _init(properties) {
+        super._init(properties);
+
+        /** @type {number} */
+        this.messageType;
+        /** @type {string} */
+        this.text;
+        /** @type {string} */
+        this.sender;
+        /** @type {number} */
+        this.timestamp;
+        /** @type {string} */
+        this.direction ;
     }
 }) : null;
 
+function newChatMessageFromTpMessage(tpMessage, direction) {
+    return new ChatMessage({
+        'message-type': tpMessage.get_message_type(),
+        'text': tpMessage.to_text()[0],
+        'sender': tpMessage.sender.alias,
+        'timestamp': direction === NotificationDirection.RECEIVED
+            ? tpMessage.get_received_timestamp() : tpMessage.get_sent_timestamp(),
+        direction,
+    });
+}
+
+function newChatMessageFromTplTextEvent(tplTextEvent) {
+    let direction =
+        tplTextEvent.get_sender().get_entity_type() === Tpl.EntityType.SELF
+            ? NotificationDirection.SENT : NotificationDirection.RECEIVED;
+
+    return new ChatMessage({
+        'message-type': tplTextEvent.get_message_type(),
+        'text': tplTextEvent.get_message(),
+        'sender': tplTextEvent.get_sender().get_alias(),
+        'timestamp': tplTextEvent.get_timestamp(),
+        direction,
+    });
+}
 
-var TelepathyComponent = class {
+export class TelepathyComponent {
     constructor() {
         this._client = null;
 
@@ -131,7 +159,7 @@ var TelepathyComponent = class {
     }
 };
 
-var TelepathyClient = HAVE_TP ? GObject.registerClass(
+export let TelepathyClient = HAVE_TP ? GObject.registerClass(
 class TelepathyClient extends Tp.BaseClient {
     _init() {
         // channel path -> ChatSource
@@ -161,11 +189,12 @@ class TelepathyClient extends Tp.BaseClient {
                       uniquify_name: true });
 
         // We only care about single-user text-based chats
-        let filter = {};
-        filter[Tp.PROP_CHANNEL_CHANNEL_TYPE] = Tp.IFACE_CHANNEL_TYPE_TEXT;
-        filter[Tp.PROP_CHANNEL_TARGET_HANDLE_TYPE] = Tp.HandleType.CONTACT;
-
+        let filter = {
+            [Tp.PROP_CHANNEL_CHANNEL_TYPE] : Tp.IFACE_CHANNEL_TYPE_TEXT,
+            [Tp.PROP_CHANNEL_TARGET_HANDLE_TYPE] : Tp.HandleType.CONTACT,
+        };
         this.set_observer_recover(true);
+        // FIXME
         this.add_observer_filter(filter);
         this.add_approver_filter(filter);
         this.add_handler_filter(filter);
@@ -201,7 +230,7 @@ class TelepathyClient extends Tp.BaseClient {
         if (this._chatSources[channel.get_object_path()])
             return;
 
-        let source = new ChatSource(account, conn, channel, contact, this);
+        let source = new ChatSource({ account, connection: conn, channel, contact, client: this });
 
         this._chatSources[channel.get_object_path()] = source;
         source.connect('destroy', () => {
@@ -294,19 +323,37 @@ class TelepathyClient extends Tp.BaseClient {
     }
 }) : null;
 
-var ChatSource = HAVE_TP ? GObject.registerClass(
+/**
+ * @typedef {object} ChatSourceParams
+ * @property {import('telepathyglib0').Account} account 
+ * @property {import('telepathyglib0').Connection} connection 
+ * @property {import('telepathyglib0').TextChannel} channel 
+ * @property {import('telepathyglib0').Contact} contact 
+ * @property {import('telepathyglib0').BaseClient} client 
+ */
+
+export let ChatSource = HAVE_TP ? GObject.registerClass(
 class ChatSource extends MessageTray.Source {
-    _init(account, conn, channel, contact, client) {
+
+    /**
+     * @param {import('../messageTray.js').SourceParams & ChatSourceParams} params 
+     */
+    _init(params) {
+        const { account, contact, client, connection, channel, ...sourceParams } = params;
+
         this._account = account;
         this._contact = contact;
         this._client = client;
 
-        super._init(contact.get_alias());
+        super._init({
+            title: contact.get_alias(),
+            ...sourceParams
+        });
 
         this.isChat = true;
         this._pendingMessages = [];
 
-        this._conn = conn;
+        this._conn = connection;
         this._channel = channel;
         this._closedId = this._channel.connect('invalidated', this._channelClosed.bind(this));
 
@@ -455,7 +502,7 @@ class ChatSource extends MessageTray.Source {
             Tpl.EventTypeMask.TEXT, SCROLLBACK_HISTORY_LINES,
             null);
 
-        let logMessages = events.map(e => ChatMessage.newFromTplTextEvent(e));
+        let logMessages = events.map(e => newChatMessageFromTplTextEvent(e));
         this._ensureNotification();
 
         let pendingTpMessages = this._channel.get_pending_messages();
@@ -467,7 +514,7 @@ class ChatSource extends MessageTray.Source {
             if (message.get_message_type() == Tp.ChannelTextMessageType.DELIVERY_REPORT)
                 continue;
 
-            pendingMessages.push(ChatMessage.newFromTpMessage(message,
+            pendingMessages.push(newChatMessageFromTpMessage(message,
                 NotificationDirection.RECEIVED));
 
             this._pendingMessages.push(message);
@@ -492,7 +539,7 @@ class ChatSource extends MessageTray.Source {
 
             if (!isPending) {
                 showTimestamp = true;
-                this._notification.appendMessage(logMessage, true, ['chat-log-message']);
+                this._notification.appendMessage(logMessage, true);
             }
         }
 
@@ -564,7 +611,7 @@ class ChatSource extends MessageTray.Source {
         this._pendingMessages.push(message);
         this.countUpdated();
 
-        message = ChatMessage.newFromTpMessage(message,
+        message = newChatMessageFromTpMessage(message,
             NotificationDirection.RECEIVED);
         this._notification.appendMessage(message);
 
@@ -590,7 +637,7 @@ class ChatSource extends MessageTray.Source {
     // our client and other clients as well.
     _messageSent(channel, message, _flags, _token) {
         this._ensureNotification();
-        message = ChatMessage.newFromTpMessage(message,
+        message = newChatMessageFromTpMessage(message,
             NotificationDirection.SENT);
         this._notification.appendMessage(message);
     }
@@ -655,19 +702,25 @@ class ChatSource extends MessageTray.Source {
 
 const ChatNotificationMessage = HAVE_TP ? GObject.registerClass(
 class ChatNotificationMessage extends GObject.Object {
+    /**
+     * @param {*} props 
+     */
     _init(props = {}) {
         super._init();
         this.set(props);
     }
 }) : null;
 
-var ChatNotification = HAVE_TP ? GObject.registerClass({
+export let ChatNotification = HAVE_TP ? GObject.registerClass({
     Signals: {
         'message-removed': { param_types: [ChatNotificationMessage.$gtype] },
         'message-added': { param_types: [ChatNotificationMessage.$gtype] },
         'timestamp-changed': { param_types: [ChatNotificationMessage.$gtype] },
     },
 }, class ChatNotification extends MessageTray.Notification {
+    /**
+     * @param {*} source 
+     */
     _init(source) {
         super._init(source, source.title, null,
             { secondaryGIcon: source.getSecondaryIcon() });
@@ -687,18 +740,13 @@ var ChatNotification = HAVE_TP ? GObject.registerClass({
 
     /**
      * appendMessage:
-     * @param {Object} message: An object with the properties
-     *   {string} message.text: the body of the message,
-     *   {Tp.ChannelTextMessageType} message.messageType: the type
-     *   {string} message.sender: the name of the sender,
-     *   {number} message.timestamp: the time the message was sent
-     *   {NotificationDirection} message.direction: a #NotificationDirection
+     * @param {ChatMessage["prototype"]} message: An object with the properties
      *
-     * @param {bool} noTimestamp: Whether to add a timestamp. If %true,
+     * @param {boolean} [noTimestamp]: Whether to add a timestamp. If %true,
      *   no timestamp will be added, regardless of the difference since
      *   the last timestamp
      */
-    appendMessage(message, noTimestamp) {
+    appendMessage(message, noTimestamp = false) {
         let messageBody = GLib.markup_escape_text(message.text, -1);
         let styles = [message.direction];
 
@@ -714,11 +762,8 @@ var ChatNotification = HAVE_TP ? GObject.registerClass({
                           bannerMarkup: true });
         }
 
-        let group = message.direction == NotificationDirection.RECEIVED
-            ? 'received' : 'sent';
-
         this._append({ body: messageBody,
-                       group,
+                       group: message.direction == NotificationDirection.RECEIVED ? 'received' : 'sent',
                        styles,
                        timestamp: message.timestamp,
                        noTimestamp });
@@ -749,15 +794,18 @@ var ChatNotification = HAVE_TP ? GObject.registerClass({
         }
     }
 
+    /** 
+     * @typedef {object} AppendProps: An object with the properties:
+     * @property {string?} body: The text of the message.
+     * @property {('received' | 'sent' | 'meta')?} group: The group of the message, one of: 'received', 
'sent', 'meta'.
+     * @property {string[]} [styles]: Style class names for the message to have.
+     * @property {number} [timestamp]: The timestamp of the message.
+     * @property {boolean} [noTimestamp]: suppress timestamp signal?
+     */
+
     /**
      * _append:
-     * @param {Object} props: An object with the properties:
-     *  {string} props.body: The text of the message.
-     *  {string} props.group: The group of the message, one of:
-     *         'received', 'sent', 'meta'.
-     *  {string[]} props.styles: Style class names for the message to have.
-     *  {number} props.timestamp: The timestamp of the message.
-     *  {bool} props.noTimestamp: suppress timestamp signal?
+     * @param {Partial<AppendProps>} props
      */
     _append(props = {}) {
         let currentTime = Date.now() / 1000;
@@ -828,16 +876,22 @@ var ChatNotification = HAVE_TP ? GObject.registerClass({
     }
 }) : null;
 
-var ChatLineBox = GObject.registerClass(
+export const ChatLineBox = GObject.registerClass(
 class ChatLineBox extends St.BoxLayout {
+    /**
+     * @returns {[number, number]} 
+     */
     vfunc_get_preferred_height(forWidth) {
         let [, natHeight] = super.vfunc_get_preferred_height(forWidth);
         return [natHeight, natHeight];
     }
 });
 
-var ChatNotificationBanner = GObject.registerClass(
+export const ChatNotificationBanner = GObject.registerClass(
 class ChatNotificationBanner extends MessageTray.NotificationBanner {
+    /**
+     * @param {*} notification 
+     */
     _init(notification) {
         super._init(notification);
 
@@ -1014,4 +1068,4 @@ class ChatNotificationBanner extends MessageTray.NotificationBanner {
     }
 });
 
-var Component = TelepathyComponent;
+export const Component = TelepathyComponent;
diff --git a/js/ui/ctrlAltTab.js b/js/ui/ctrlAltTab.js
index 6507886a40..3735964f90 100644
--- a/js/ui/ctrlAltTab.js
+++ b/js/ui/ctrlAltTab.js
@@ -1,20 +1,24 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported CtrlAltTabManager */
 
-const { Clutter, GObject, Meta, Shell, St } = imports.gi;
+import Clutter from 'gi://Clutter';
+import GObject from 'gi://GObject';
+import Meta from 'gi://Meta';
+import Shell from 'gi://Shell';
+import St from 'gi://St';
 
-const Main = imports.ui.main;
-const SwitcherPopup = imports.ui.switcherPopup;
+import Main from './main.js';
+import * as SwitcherPopup from './switcherPopup.js';
 
-var POPUP_APPICON_SIZE = 96;
+export let POPUP_APPICON_SIZE = 96;
 
-var SortGroup = {
+export const SortGroup = {
     TOP:    0,
     MIDDLE: 1,
     BOTTOM: 2,
 };
 
-var CtrlAltTabManager = class CtrlAltTabManager {
+export class CtrlAltTabManager {
     constructor() {
         this._items = [];
         this.addGroup(global.window_group, _("Windows"),
@@ -138,7 +142,7 @@ var CtrlAltTabManager = class CtrlAltTabManager {
     }
 };
 
-var CtrlAltTabPopup = GObject.registerClass(
+export const CtrlAltTabPopup = GObject.registerClass(
 class CtrlAltTabPopup extends SwitcherPopup.SwitcherPopup {
     _init(items) {
         super._init(items);
@@ -167,7 +171,7 @@ class CtrlAltTabPopup extends SwitcherPopup.SwitcherPopup {
     }
 });
 
-var CtrlAltTabSwitcher = GObject.registerClass(
+export const CtrlAltTabSwitcher = GObject.registerClass(
 class CtrlAltTabSwitcher extends SwitcherPopup.SwitcherList {
     _init(items) {
         super._init(true);
diff --git a/js/ui/dash.js b/js/ui/dash.js
index 08902c436a..6a083fad73 100644
--- a/js/ui/dash.js
+++ b/js/ui/dash.js
@@ -1,29 +1,34 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported Dash */
 
-const { Clutter, GLib, GObject,
-        Graphene, Meta, Shell, St } = imports.gi;
-
-const AppDisplay = imports.ui.appDisplay;
-const AppFavorites = imports.ui.appFavorites;
-const DND = imports.ui.dnd;
-const IconGrid = imports.ui.iconGrid;
-const Main = imports.ui.main;
-const Overview = imports.ui.overview;
-
-var DASH_ANIMATION_TIME = 200;
-var DASH_ITEM_LABEL_SHOW_TIME = 150;
-var DASH_ITEM_LABEL_HIDE_TIME = 100;
-var DASH_ITEM_HOVER_TIMEOUT = 300;
-
-function getAppFromSource(source) {
+import Clutter from 'gi://Clutter';
+import GLib from 'gi://GLib';
+import GObject from 'gi://GObject';
+import Graphene from 'gi://Graphene';
+import Meta from 'gi://Meta';
+import Shell from 'gi://Shell';
+import St from 'gi://St';
+
+import * as AppDisplay from './appDisplay.js';
+import * as AppFavorites from './appFavorites.js';
+import * as DND from './dnd.js';
+import * as IconGrid from './iconGrid.js';
+import * as Overview from './overview.js';
+import Main from './main.js';
+
+export const DASH_ANIMATION_TIME = 200;
+export const DASH_ITEM_LABEL_SHOW_TIME = 150;
+export const DASH_ITEM_LABEL_HIDE_TIME = 100;
+export const DASH_ITEM_HOVER_TIMEOUT = 300;
+
+export function getAppFromSource(source) {
     if (source instanceof AppDisplay.AppIcon)
         return source.app;
     else
         return null;
 }
 
-var DashIcon = GObject.registerClass(
+export const DashIcon = GObject.registerClass(
 class DashIcon extends AppDisplay.AppIcon {
     _init(app) {
         super._init(app, {
@@ -54,7 +59,7 @@ class DashIcon extends AppDisplay.AppIcon {
 
 // A container like StBin, but taking the child's scale into account
 // when requesting a size
-var DashItemContainer = GObject.registerClass(
+export const DashItemContainer = GObject.registerClass(
 class DashItemContainer extends St.Widget {
     _init() {
         super._init({
@@ -74,6 +79,7 @@ class DashItemContainer extends St.Widget {
         Main.layoutManager.addChrome(this.label);
         this.label_actor = this.label;
 
+        /** @type {St.Bin | null} */
         this.child = null;
         this.animatingOut = false;
 
@@ -146,6 +152,9 @@ class DashItemContainer extends St.Widget {
         });
     }
 
+    /**
+     * @param {St.Bin} actor 
+     */
     setChild(actor) {
         if (this.child == actor)
             return;
@@ -191,7 +200,7 @@ class DashItemContainer extends St.Widget {
     }
 });
 
-var ShowAppsIcon = GObject.registerClass(
+export const ShowAppsIcon = GObject.registerClass(
 class ShowAppsIcon extends DashItemContainer {
     _init() {
         super._init();
@@ -270,7 +279,7 @@ class ShowAppsIcon extends DashItemContainer {
     }
 });
 
-var DragPlaceholderItem = GObject.registerClass(
+export const DragPlaceholderItem = GObject.registerClass(
 class DragPlaceholderItem extends DashItemContainer {
     _init() {
         super._init();
@@ -278,7 +287,7 @@ class DragPlaceholderItem extends DashItemContainer {
     }
 });
 
-var EmptyDropTargetItem = GObject.registerClass(
+export const EmptyDropTargetItem = GObject.registerClass(
 class EmptyDropTargetItem extends DashItemContainer {
     _init() {
         super._init();
@@ -294,6 +303,7 @@ class DashIconsLayout extends Clutter.BoxLayout {
         });
     }
 
+    /** @returns {[number, number]} */
     vfunc_get_preferred_width(container, forHeight) {
         const [, natWidth] = super.vfunc_get_preferred_width(container, forHeight);
         return [0, natWidth];
@@ -302,7 +312,7 @@ class DashIconsLayout extends Clutter.BoxLayout {
 
 const baseIconSizes = [16, 22, 24, 32, 48, 64];
 
-var Dash = GObject.registerClass({
+export const Dash = GObject.registerClass({
     Signals: { 'icon-size-changed': {} },
 }, class Dash extends St.Widget {
     _init() {
@@ -330,6 +340,7 @@ var Dash = GObject.registerClass({
             y_expand: true,
         });
 
+        /** @type {St.Widget<DashIconsLayout["prototype"], Clutter.Content, St.Widget | 
DashItemContainer["prototype"]>} */
         this._box = new St.Widget({
             clip_to_allocation: true,
             layout_manager: new DashIconsLayout(),
@@ -507,6 +518,10 @@ var Dash = GObject.registerClass({
                         });
 
         let item = new DashItemContainer();
+
+        // TODO: I don't know why this type error is occuring.
+        //       I'm guessing it is an issue with the complicated inheritance.
+        // @ts-expect-error
         item.setChild(appIcon);
 
         // Override default AppIcon label_actor, now the
@@ -574,9 +589,16 @@ var Dash = GObject.registerClass({
         // icons (i.e. ignoring drag placeholders) and which are not
         // animating out (which means they will be destroyed at the end of
         // the animation)
-        let iconChildren = this._box.get_children().filter(actor => {
-            return actor.child &&
+        let iconChildren = this._box.get_children().filter(
+            /**
+             * @param {Clutter.Actor} actor 
+             * @returns {actor is DashItemContainer["prototype"]}
+             */
+            (actor) => {
+            return actor instanceof DashItemContainer && 
+                   actor.child &&
                    actor.child._delegate &&
+                   // @ts-expect-error
                    actor.child._delegate.icon &&
                    !actor.animatingOut;
         });
@@ -598,7 +620,7 @@ var Dash = GObject.registerClass({
         let spacing = themeNode.get_length('spacing');
 
         let firstButton = iconChildren[0].child;
-        let firstIcon = firstButton._delegate.icon;
+        let firstIcon = /** @type {typeof AppDisplay.AppIcon["prototype"]} */ (firstButton._delegate).icon;
 
         // Enforce valid spacings during the size request
         firstIcon.icon.ensure_style();
@@ -634,7 +656,7 @@ var Dash = GObject.registerClass({
 
         let scale = oldIconSize / newIconSize;
         for (let i = 0; i < iconChildren.length; i++) {
-            let icon = iconChildren[i].child._delegate.icon;
+            let icon = /** @type {typeof AppDisplay.AppIcon["prototype"]} */ 
(iconChildren[i].child._delegate).icon;
 
             // Set the new size immediately, to keep the icons' sizes
             // in sync with this.iconSize
@@ -676,13 +698,20 @@ var Dash = GObject.registerClass({
 
         let running = this._appSystem.get_running();
 
+        // FIXME
         let children = this._box.get_children().filter(actor => {
-            return actor.child &&
-                   actor.child._delegate &&
-                   actor.child._delegate.app;
+            return 'child' in actor && actor.child &&
+                   /** @type {typeof AppDisplay.AppIcon["prototype"]} */ (actor.child._delegate).app &&
+                   /** @type {typeof AppDisplay.AppIcon["prototype"]} */ (actor.child._delegate).app;
+                   
         });
         // Apps currently in the dash
-        let oldApps = children.map(actor => actor.child._delegate.app);
+        let oldApps = children.map(actor => 'child' in actor ? actor.child._delegate : null).filter(
+            /**
+             * @param {unknown} delegate '
+             * @returns {delegate is typeof AppDisplay.AppIcon["prototype"]}
+             */
+            delegate => delegate instanceof AppDisplay.AppIcon).map(d => d.app);
         // Apps supposed to be in the dash
         let newApps = [];
 
@@ -749,7 +778,8 @@ var Dash = GObject.registerClass({
                 ? newApps[newIndex + 1] : null;
             let insertHere = nextApp && nextApp == oldApp;
             let alreadyRemoved = removedActors.reduce((result, actor) => {
-                let removedApp = actor.child._delegate.app;
+                let removedApp = /** @type {typeof AppDisplay.AppIcon["prototype"]} */ 
(actor.child._delegate).app;
+
                 return result || removedApp == newApp;
             }, false);
 
@@ -773,9 +803,10 @@ var Dash = GObject.registerClass({
         for (let i = 0; i < removedActors.length; i++) {
             let item = removedActors[i];
 
+            // FIXME
             // Don't animate item removal when the overview is transitioning
             // or hidden
-            if (Main.overview.visible && !Main.overview.animationInProgress)
+            if (Main.overview.visible && !Main.overview.animationInProgress && 'animateOutAndDestroy' in 
item)
                 item.animateOutAndDestroy();
             else
                 item.destroy();
@@ -947,7 +978,7 @@ var Dash = GObject.registerClass({
                 children[i] == this._dragPlaceholder)
                 continue;
 
-            let childId = children[i].child._delegate.app.get_id();
+            let childId = /** @type {typeof AppDisplay.AppIcon["prototype"]} */ 
(children[i].child._delegate).app.get_id();
             if (childId == id)
                 continue;
             if (childId in favorites)
diff --git a/js/ui/dateMenu.js b/js/ui/dateMenu.js
index d202e84363..488c1ee9da 100644
--- a/js/ui/dateMenu.js
+++ b/js/ui/dateMenu.js
@@ -1,17 +1,24 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported DateMenuButton */
 
-const { Clutter, Gio, GLib, GnomeDesktop,
-        GObject, GWeather, Pango, Shell, St } = imports.gi;
-
-const Util = imports.misc.util;
-const Main = imports.ui.main;
-const PanelMenu = imports.ui.panelMenu;
-const Calendar = imports.ui.calendar;
-const Weather = imports.misc.weather;
-const System = imports.system;
-
-const { loadInterfaceXML } = imports.misc.fileUtils;
+import Clutter from 'gi://Clutter';
+import Gio from 'gi://Gio';
+import GLib from 'gi://GLib';
+import GnomeDesktop from 'gi://GnomeDesktop';
+import GObject from 'gi://GObject';
+import GWeather from 'gi://GWeather';
+import Pango from 'gi://Pango';
+import Shell from 'gi://Shell';
+import St from 'gi://St';
+
+import * as Util from '../misc/util.js';
+import Main from './main.js';
+import * as PanelMenu from './panelMenu.js';
+import * as Calendar from './calendar.js';
+import * as Weather from '../misc/weather.js';
+import System from 'system';
+
+import { loadInterfaceXML } from '../misc/fileUtilsModule.js';
 
 const NC_ = (context, str) => '%s\u0004%s'.format(context, str);
 const T_ = Shell.util_translate_time_string;
@@ -24,7 +31,7 @@ const ClocksProxy = Gio.DBusProxy.makeProxyWrapper(ClocksIntegrationIface);
 
 function _isToday(date) {
     let now = new Date();
-    return now.getYear() == date.getYear() &&
+    return now.getFullYear() == date.getFullYear() &&
            now.getMonth() == date.getMonth() &&
            now.getDate() == date.getDate();
 }
@@ -33,8 +40,11 @@ function _gDateTimeToDate(datetime) {
     return new Date(datetime.to_unix() * 1000 + datetime.get_microsecond() / 1000);
 }
 
-var TodayButton = GObject.registerClass(
+export const TodayButton = GObject.registerClass(
 class TodayButton extends St.Button {
+    /**
+     * @param {*} calendar 
+     */
     _init(calendar) {
         // Having the ability to go to the current date if the user is already
         // on the current date can be confusing. So don't make the button reactive
@@ -88,7 +98,7 @@ class TodayButton extends St.Button {
     }
 });
 
-var EventsSection = GObject.registerClass(
+export const EventsSection = GObject.registerClass(
 class EventsSection extends St.Button {
     _init() {
         super._init({
@@ -126,7 +136,11 @@ class EventsSection extends St.Button {
         this._appInstalledChanged();
     }
 
+    /**
+     * @param {Date} date 
+     */
     setDate(date) {
+        /** @type {[number, number, number]} */
         const day = [date.getFullYear(), date.getMonth(), date.getDate()];
         this._startDate = new Date(...day);
         this._endDate = new Date(...day, 23, 59, 59, 999);
@@ -158,9 +172,9 @@ class EventsSection extends St.Button {
 
         if (this._startDate <= now && now <= this._endDate)
             this._title.text = _('Today');
-        else if (this._endDate < now && now - this._endDate < timeSpanDay)
+        else if (this._endDate < now && now.getTime() - this._endDate.getTime() < timeSpanDay)
             this._title.text = _('Yesterday');
-        else if (this._startDate > now && this._startDate - now < timeSpanDay)
+        else if (this._startDate > now && this._startDate.getTime() - now.getTime() < timeSpanDay)
             this._title.text = _('Tomorrow');
         else if (this._startDate.getFullYear() === now.getFullYear())
             this._title.text = this._startDate.toLocaleFormat(sameYearFormat);
@@ -270,7 +284,7 @@ class EventsSection extends St.Button {
     }
 });
 
-var WorldClocksSection = GObject.registerClass(
+export const WorldClocksSection = GObject.registerClass(
 class WorldClocksSection extends St.Button {
     _init() {
         super._init({
@@ -331,7 +345,7 @@ class WorldClocksSection extends St.Button {
         this._locations = [];
 
         let world = GWeather.Location.get_world();
-        let clocks = this._settings.get_value('locations').deep_unpack();
+        let clocks = /** @type {GLib.Variant<'av'>} */ (this._settings.get_value('locations')).deep_unpack();
         for (let i = 0; i < clocks.length; i++) {
             let l = world.deserialize(clocks[i]);
             if (l && l.get_timezone() != null)
@@ -466,7 +480,7 @@ class WorldClocksSection extends St.Button {
     }
 });
 
-var WeatherSection = GObject.registerClass(
+export const WeatherSection = GObject.registerClass(
 class WeatherSection extends St.Button {
     _init() {
         super._init({
@@ -602,6 +616,9 @@ class WeatherSection extends St.Button {
         layout.attach(label, 0, 0, 1, 1);
     }
 
+    /**
+     * @param {GWeather.Location} loc 
+     */
     _findBestLocationName(loc) {
         const locName = loc.get_name();
 
@@ -659,7 +676,7 @@ class WeatherSection extends St.Button {
     }
 });
 
-var MessagesIndicator = GObject.registerClass(
+export const MessagesIndicator = GObject.registerClass(
 class MessagesIndicator extends St.Icon {
     _init() {
         super._init({
@@ -720,13 +737,17 @@ class MessagesIndicator extends St.Icon {
     }
 });
 
-var FreezableBinLayout = GObject.registerClass(
+export const FreezableBinLayout = GObject.registerClass(
 class FreezableBinLayout extends Clutter.BinLayout {
     _init() {
         super._init();
 
         this._frozen = false;
+        
+        /** @type {[number, number]} */
         this._savedWidth = [NaN, NaN];
+        
+        /** @type {[number, number]} */
         this._savedHeight = [NaN, NaN];
     }
 
@@ -739,6 +760,9 @@ class FreezableBinLayout extends Clutter.BinLayout {
             this.layout_changed();
     }
 
+    /**
+     * @returns {[number, number]} 
+     */
     vfunc_get_preferred_width(container, forHeight) {
         if (!this._frozen || this._savedWidth.some(isNaN))
             return super.vfunc_get_preferred_width(container, forHeight);
@@ -760,8 +784,11 @@ class FreezableBinLayout extends Clutter.BinLayout {
     }
 });
 
-var CalendarColumnLayout = GObject.registerClass(
+export const CalendarColumnLayout = GObject.registerClass(
 class CalendarColumnLayout extends Clutter.BoxLayout {
+    /**
+     * @param {*} actors 
+     */
     _init(actors) {
         super._init({ orientation: Clutter.Orientation.VERTICAL });
         this._colActors = actors;
@@ -779,7 +806,7 @@ class CalendarColumnLayout extends Clutter.BoxLayout {
     }
 });
 
-var DateMenuButton = GObject.registerClass(
+export const DateMenuButton = GObject.registerClass(
 class DateMenuButton extends PanelMenu.Button {
     _init() {
         let hbox;
diff --git a/js/ui/dialog.js b/js/ui/dialog.js
index 9513a8151a..0a8e448eae 100644
--- a/js/ui/dialog.js
+++ b/js/ui/dialog.js
@@ -1,7 +1,13 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported Dialog, MessageDialogContent, ListSection, ListSectionItem */
 
-const { Clutter, GLib, GObject, Meta, Pango, St } = imports.gi;
+import Clutter from 'gi://Clutter';
+import GLib from 'gi://GLib';
+import GObject from 'gi://GObject';
+import Meta from 'gi://Meta';
+import Pango from 'gi://Pango';
+import St from 'gi://St';
+
 
 function _setLabel(label, value) {
     label.set({
@@ -10,8 +16,12 @@ function _setLabel(label, value) {
     });
 }
 
-var Dialog = GObject.registerClass(
+export const Dialog = GObject.registerClass(
 class Dialog extends St.Widget {
+    /**
+     * @param {*} parentActor 
+     * @param {*} styleClass 
+     */
     _init(parentActor, styleClass) {
         super._init({ layout_manager: new Clutter.BinLayout() });
         this.connect('destroy', this._onDestroy.bind(this));
@@ -157,7 +167,7 @@ class Dialog extends St.Widget {
     }
 });
 
-var MessageDialogContent = GObject.registerClass({
+export const MessageDialogContent = GObject.registerClass({
     Properties: {
         'title': GObject.ParamSpec.string(
             'title', 'title', 'title',
@@ -171,6 +181,9 @@ var MessageDialogContent = GObject.registerClass({
             null),
     },
 }, class MessageDialogContent extends St.BoxLayout {
+    /**
+     * @param {*} params 
+     */
     _init(params) {
         this._title = new St.Label({ style_class: 'message-dialog-title' });
         this._description = new St.Label({ style_class: 'message-dialog-description' });
@@ -247,7 +260,7 @@ var MessageDialogContent = GObject.registerClass({
     }
 });
 
-var ListSection = GObject.registerClass({
+export const ListSection = GObject.registerClass({
     Properties: {
         'title': GObject.ParamSpec.string(
             'title', 'title', 'title',
@@ -256,6 +269,9 @@ var ListSection = GObject.registerClass({
             null),
     },
 }, class ListSection extends St.BoxLayout {
+    /**
+     * @param {*} params 
+     */
     _init(params) {
         this._title = new St.Label({ style_class: 'dialog-list-title' });
 
@@ -292,7 +308,7 @@ var ListSection = GObject.registerClass({
     }
 });
 
-var ListSectionItem = GObject.registerClass({
+export const ListSectionItem = GObject.registerClass({
     Properties: {
         'icon-actor':  GObject.ParamSpec.object(
             'icon-actor', 'icon-actor', 'Icon actor',
@@ -310,6 +326,9 @@ var ListSectionItem = GObject.registerClass({
             null),
     },
 }, class ListSectionItem extends St.BoxLayout {
+    /**
+     * @param {*} params 
+     */
     _init(params) {
         this._iconActorBin = new St.Bin();
 
diff --git a/js/ui/dnd.js b/js/ui/dnd.js
index a659c4fd21..3da06629b3 100644
--- a/js/ui/dnd.js
+++ b/js/ui/dnd.js
@@ -1,43 +1,87 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported addDragMonitor, removeDragMonitor, makeDraggable */
 
-const { Clutter, GLib, Meta, Shell, St } = imports.gi;
-const Signals = imports.misc.signals;
+import Clutter from 'gi://Clutter';
+import GLib from 'gi://GLib';
+import Meta from 'gi://Meta';
+import Shell from 'gi://Shell';
+import St from 'gi://St';
 
-const Main = imports.ui.main;
+import * as Signals from '../misc/signals.js';
+
+import Main from './main.js';
 
 // Time to scale down to maxDragActorSize
-var SCALE_ANIMATION_TIME = 250;
+export let SCALE_ANIMATION_TIME = 250;
 // Time to animate to original position on cancel
-var SNAP_BACK_ANIMATION_TIME = 250;
+export let SNAP_BACK_ANIMATION_TIME = 250;
 // Time to animate to original position on success
-var REVERT_ANIMATION_TIME = 750;
+export let REVERT_ANIMATION_TIME = 750;
+
+/** @typedef {{ getDragActor<T extends Clutter.Actor = Clutter.Actor>(): T }} DragActorContainer */
+/** @typedef {{ getDragActorSource<T extends Clutter.Actor = Clutter.Actor>(): T }} DragActorSourceContainer 
*/
+/** @typedef {{ handleDragOver(source: Clutter.Actor | Signals.EventEmitter, actor: Clutter.Actor, x: 
number, y: number, time: number): DragMotionResult }} DragOverTarget */
+/** @typedef {{ acceptDrop(source: Clutter.Actor | Signals.EventEmitter, actor: Clutter.Actor, x: number, y: 
number, time: number): boolean }} DropTarget */
+
+/**
+ * @param {unknown} delegate
+ * @returns {delegate is DragActorContainer} 
+ */
+function hasDragActor(delegate) {
+    return !!(/** @type {DragActorContainer} */ (delegate).getDragActor);
+}
+
+/**
+ * @param {unknown} delegate
+ * @returns {delegate is DragActorSourceContainer} 
+ */
+function hasDragActorSource(delegate) {
+    return !!(/** @type {DragActorSourceContainer} */ (delegate).getDragActorSource);
+}
+
+/**
+ * @param {unknown} delegate
+ * @returns {delegate is DragOverTarget} 
+ */
+export function handlesDragOver(delegate) {
+    return !!(/** @type {DragOverTarget} */ (delegate).handleDragOver);
+}
 
-var DragMotionResult = {
+/**
+ * @param {unknown} delegate
+ * @returns {delegate is DropTarget} 
+ */
+function isDropTarget(delegate) {
+    return !!(/** @type {DropTarget} */ (delegate).acceptDrop);
+}
+
+/** @enum {number} */
+export const DragMotionResult = {
     NO_DROP:   0,
     COPY_DROP: 1,
     MOVE_DROP: 2,
     CONTINUE:  3,
 };
 
-var DragState = {
+/** @enum {number} */
+export const DragState = {
     INIT:      0,
     DRAGGING:  1,
     CANCELLED: 2,
 };
 
-var DRAG_CURSOR_MAP = {
+export const DRAG_CURSOR_MAP = {
     0: Meta.Cursor.DND_UNSUPPORTED_TARGET,
     1: Meta.Cursor.DND_COPY,
     2: Meta.Cursor.DND_MOVE,
 };
 
-var DragDropResult = {
+export const DragDropResult = {
     FAILURE:  0,
     SUCCESS:  1,
     CONTINUE: 2,
 };
-var dragMonitors = [];
+export let dragMonitors = [];
 
 let eventHandlerActor = null;
 let currentDraggable = null;
@@ -64,11 +108,11 @@ function _getRealActorScale(actor) {
     return scale;
 }
 
-function addDragMonitor(monitor) {
+export function addDragMonitor(monitor) {
     dragMonitors.push(monitor);
 }
 
-function removeDragMonitor(monitor) {
+export function removeDragMonitor(monitor) {
     for (let i = 0; i < dragMonitors.length; i++) {
         if (dragMonitors[i] == monitor) {
             dragMonitors.splice(i, 1);
@@ -77,7 +121,20 @@ function removeDragMonitor(monitor) {
     }
 }
 
-var _Draggable = class _Draggable extends Signals.EventEmitter {
+// FIXME
+/**
+ * @typedef {object} _DraggableParams
+ * @property {boolean} [manualMode]
+ * @property {number} [timeoutThreshold]
+ * @property {boolean} [restoreOnSuccess]
+ * @property {number} [dragActorMaxSize]
+ * @property {number} [dragActorOpacity]
+ */
+export class _Draggable extends Signals.EventEmitter {
+    /**
+     * @param {Clutter.Actor} actor
+     * @param {Partial<_DraggableParams>} [params]
+     */
     constructor(actor, params = {}) {
         super();
 
@@ -358,7 +415,7 @@ var _Draggable = class _Draggable extends Signals.EventEmitter {
 
         let scaledWidth, scaledHeight;
 
-        if (this.actor._delegate && this.actor._delegate.getDragActor) {
+        if (this.actor._delegate && hasDragActor(this.actor._delegate)) {
             this._dragActor = this.actor._delegate.getDragActor();
             Main.uiGroup.add_child(this._dragActor);
             Main.uiGroup.set_child_above_sibling(this._dragActor, null);
@@ -367,7 +424,7 @@ var _Draggable = class _Draggable extends Signals.EventEmitter {
             // Drag actor does not always have to be the same as actor. For example drag actor
             // can be an image that's part of the actor. So to perform "snap back" correctly we need
             // to know what was the drag actor source.
-            if (this.actor._delegate.getDragActorSource) {
+            if (hasDragActorSource(this.actor._delegate)) {
                 this._dragActorSource = this.actor._delegate.getDragActorSource();
                 // If the user dragged from the source, then position
                 // the dragActor over it. Otherwise, center it
@@ -570,7 +627,7 @@ var _Draggable = class _Draggable extends Signals.EventEmitter {
         dragEvent.targetActor.disconnect(targetActorDestroyHandlerId);
 
         while (target) {
-            if (target._delegate && target._delegate.handleDragOver) {
+            if (target._delegate && handlesDragOver(target._delegate)) {
                 let [r_, targX, targY] = target.transform_stage_point(this._dragX, this._dragY);
                 // We currently loop through all parents on drag-over even if one of the children has 
handled it.
                 // We can check the return value of the function and break the loop if it's true if we don't 
want
@@ -643,7 +700,7 @@ var _Draggable = class _Draggable extends Signals.EventEmitter {
         this._dragCancellable = false;
 
         while (target) {
-            if (target._delegate && target._delegate.acceptDrop) {
+            if (target._delegate && isDropTarget(target._delegate)) {
                 let [r_, targX, targY] = target.transform_stage_point(dropX, dropY);
                 let accepted = false;
                 try {
@@ -842,6 +899,6 @@ var _Draggable = class _Draggable extends Signals.EventEmitter {
  * target wants to reuse the actor, it's up to the drop target to
  * reset these values.
  */
-function makeDraggable(actor, params) {
+export function makeDraggable(actor, params) {
     return new _Draggable(actor, params);
 }
diff --git a/js/ui/edgeDragAction.js b/js/ui/edgeDragAction.js
index 986b658e06..148dda2325 100644
--- a/js/ui/edgeDragAction.js
+++ b/js/ui/edgeDragAction.js
@@ -1,19 +1,26 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported EdgeDragAction */
 
-const { Clutter, GObject, Meta, St } = imports.gi;
+import Clutter from 'gi://Clutter';
+import GObject from 'gi://GObject';
+import Meta from 'gi://Meta';
+import St from 'gi://St';
 
-const Main = imports.ui.main;
+import Main from './main.js';
 
-var EDGE_THRESHOLD = 20;
-var DRAG_DISTANCE = 80;
+export const EDGE_THRESHOLD = 20;
+export const DRAG_DISTANCE = 80;
 
-var EdgeDragAction = GObject.registerClass({
+export const EdgeDragAction = GObject.registerClass({
     Signals: {
         'activated': {},
         'progress': { param_types: [GObject.TYPE_DOUBLE] },
     },
 }, class EdgeDragAction extends Clutter.GestureAction {
+    /**
+     * @param {*} side 
+     * @param {*} allowedModes 
+     */
     _init(side, allowedModes) {
         super._init();
         this._side = side;
diff --git a/js/ui/endSessionDialog.js b/js/ui/endSessionDialog.js
index c2a3142063..34a19d52e1 100644
--- a/js/ui/endSessionDialog.js
+++ b/js/ui/endSessionDialog.js
@@ -17,17 +17,26 @@
  * along with this program; if not, see <http://www.gnu.org/licenses/>.
  */
 
-const { AccountsService, Clutter, Gio,
-        GLib, GObject, Pango, Polkit, Shell, St, UPowerGlib: UPower }  = imports.gi;
-
-const CheckBox = imports.ui.checkBox;
-const Dialog = imports.ui.dialog;
-const GnomeSession = imports.misc.gnomeSession;
-const LoginManager = imports.misc.loginManager;
-const ModalDialog = imports.ui.modalDialog;
-const UserWidget = imports.ui.userWidget;
-
-const { loadInterfaceXML } = imports.misc.fileUtils;
+import Clutter from 'gi://Clutter';
+import GLib from 'gi://GLib';
+import GObject from 'gi://GObject';
+import Gio from 'gi://Gio';
+
+import AccountsService from 'gi://AccountsService';
+import Pango from 'gi://Pango';
+import Polkit from 'gi://Polkit';
+import Shell from 'gi://Shell';
+import St from 'gi://St';
+import UPower from 'gi://UPowerGlib';
+
+import * as CheckBox from './checkBox.js';
+import * as Dialog from './dialog.js';
+import * as GnomeSession from '../misc/gnomeSession.js';
+import * as LoginManager from '../misc/loginManager.js';
+import * as ModalDialog from './modalDialog.js';
+import * as UserWidget from './userWidget.js';
+
+import { loadInterfaceXML } from '../misc/fileUtilsModule.js';
 
 const _ITEM_ICON_SIZE = 64;
 
@@ -151,7 +160,7 @@ const DialogContent = {
     4 /* DialogType.UPGRADE_RESTART */: restartUpgradeDialogContent,
 };
 
-var MAX_USERS_IN_SESSION_DIALOG = 5;
+export let MAX_USERS_IN_SESSION_DIALOG = 5;
 
 const LogindSessionIface = loadInterfaceXML('org.freedesktop.login1.Session');
 const LogindSession = Gio.DBusProxy.makeProxyWrapper(LogindSessionIface);
@@ -216,14 +225,22 @@ function _setCheckBoxLabel(checkBox, text) {
     }
 }
 
-function init() {
+export function init() {
     // This always returns the same singleton object
     // By instantiating it initially, we register the
     // bus object, etc.
     new EndSessionDialog();
 }
 
-var EndSessionDialog = GObject.registerClass(
+/** @typedef {{ name?: string; version?: string; }} PreparedUpgradeInfo */
+/** @typedef {{
+    UpdateTriggered: boolean,
+    UpdatePrepared: boolean,
+    UpgradeTriggered: boolean,
+    PreparedUpgrade: PreparedUpgradeInfo }
+} UpdateInfo */
+
+export const EndSessionDialog = GObject.registerClass(
 class EndSessionDialog extends ModalDialog.ModalDialog {
     _init() {
         super._init({ styleClass: 'end-session-dialog',
@@ -650,7 +667,7 @@ class EndSessionDialog extends ModalDialog.ModalDialog {
             let n = 0;
             for (let i = 0; i < result.length; i++) {
                 let [id_, uid_, userName, seat_, sessionPath] = result[i];
-                let proxy = new LogindSession(Gio.DBus.system, 'org.freedesktop.login1', sessionPath);
+                let proxy = LogindSession(Gio.DBus.system, 'org.freedesktop.login1', sessionPath);
 
                 if (proxy.Class != 'user')
                     continue;
@@ -662,7 +679,7 @@ class EndSessionDialog extends ModalDialog.ModalDialog {
                 if (!sessionId) {
                     this._loginManager.getCurrentSessionProxy(currentSessionProxy => {
                         sessionId = currentSessionProxy.Id;
-                        log('endSessionDialog: No XDG_SESSION_ID, fetched from logind: 
%d'.format(sessionId));
+                        log('endSessionDialog: No XDG_SESSION_ID, fetched from logind: 
%s'.format(sessionId));
                     });
                 }
 
@@ -706,6 +723,9 @@ class EndSessionDialog extends ModalDialog.ModalDialog {
         });
     }
 
+    /**
+     * @returns {Promise<UpdateInfo>}
+     */
     async _getUpdateInfo() {
         const connection = this._pkOfflineProxy.get_connection();
         const reply = await connection.call(
@@ -714,12 +734,14 @@ class EndSessionDialog extends ModalDialog.ModalDialog {
             'org.freedesktop.DBus.Properties',
             'GetAll',
             new GLib.Variant('(s)', [this._pkOfflineProxy.g_interface_name]),
-            null,
+            new GLib.VariantType('(a{sv})'),
             Gio.DBusCallFlags.NONE,
             -1,
             null);
+
         const [info] = reply.recursiveUnpack();
-        return info;
+
+        return /** @type {UpdateInfo} */ (info);
     }
 
     async OpenAsync(parameters, invocation) {
@@ -764,7 +786,7 @@ class EndSessionDialog extends ModalDialog.ModalDialog {
         let dialogContent = DialogContent[this._type];
 
         for (let i = 0; i < inhibitorObjectPaths.length; i++) {
-            let inhibitor = new GnomeSession.Inhibitor(inhibitorObjectPaths[i], proxy => {
+            let inhibitor = GnomeSession.Inhibitor(inhibitorObjectPaths[i], proxy => {
                 this._onInhibitorLoaded(proxy);
             });
 
diff --git a/js/ui/environment.js b/js/ui/environment.js
index ccd7dcdaa6..16b3894421 100644
--- a/js/ui/environment.js
+++ b/js/ui/environment.js
@@ -3,27 +3,34 @@
 
 const Config = imports.misc.config;
 
-imports.gi.versions.Clutter = Config.LIBMUTTER_API_VERSION;
-imports.gi.versions.Gio = '2.0';
-imports.gi.versions.GdkPixbuf = '2.0';
-imports.gi.versions.Gtk = '3.0';
-imports.gi.versions.Soup = '3.0';
-imports.gi.versions.TelepathyGLib = '0.12';
-imports.gi.versions.TelepathyLogger = '0.2';
+import gi from "gi";
+import "gi://Gio?version=2.0";
+import "gi://GdkPixbuf?version=2.0";
+import "gi://Gtk?version=3.0";
+import "gi://TelepathyGLib?version=0.12";
+import "gi://TelepathyLogger?version=0.2";
+
+import Gio from "gi://Gio";
+import GLib from "gi://GLib";
+import GObject from "gi://GObject";
+import Meta from "gi://Meta";
+import Polkit from "gi://Polkit";
+import Shell from "gi://Shell";
+import St from "gi://St";
+import * as Gettext from "gettext";
+import System from "system";
+// import * as ExtensionUtils from '../misc/extensionUtils.js';
 
 try {
-    if (Config.HAVE_SOUP2)
-        throw new Error('Soup3 support not enabled');
-    const Soup_ = imports.gi.Soup;
+  if (Config.HAVE_SOUP2)
+      throw new Error('Soup3 support not enabled');
+  const Soup_ = gi.require('Soup', '3.0');
 } catch (e) {
-    imports.gi.versions.Soup = '2.4';
-    const { Soup } = imports.gi;
-    _injectSoup3Compat(Soup);
+  const Soup = gi.require('Soup', '2.4');
+  _injectSoup3Compat(Soup);
 }
 
-const { Clutter, Gio, GLib, GObject, Meta, Polkit, Shell, St } = imports.gi;
-const Gettext = imports.gettext;
-const System = imports.system;
+const Clutter = gi.require("Clutter", Config.LIBMUTTER_API_VERSION);
 
 Gio._promisify(Gio.DataInputStream.prototype, 'fill_async', 'fill_finish');
 Gio._promisify(Gio.DataInputStream.prototype,
@@ -42,41 +49,37 @@ let _localTimeZone = null;
 // variable initializations, etc, that depend on init() already having
 // been run.
 
-
 // "monkey patch" in some varargs ClutterContainer methods; we need
 // to do this per-container class since there is no representation
 // of interfaces in Javascript
 function _patchContainerClass(containerClass) {
-    // This one is a straightforward mapping of the C method
-    containerClass.prototype.child_set = function (actor, props) {
-        let meta = this.get_child_meta(actor);
-        for (let prop in props)
-            meta[prop] = props[prop];
-    };
-
-    // clutter_container_add() actually is a an add-many-actors
-    // method. We conveniently, but somewhat dubiously, take the
-    // this opportunity to make it do something more useful.
-    containerClass.prototype.add = function (actor, props) {
-        this.add_actor(actor);
-        if (props)
-            this.child_set(actor, props);
-    };
+  // This one is a straightforward mapping of the C method
+  containerClass.prototype.child_set = function (actor, props) {
+    let meta = this.get_child_meta(actor);
+    for (let prop in props) meta[prop] = props[prop];
+  };
+
+  // clutter_container_add() actually is a an add-many-actors
+  // method. We conveniently, but somewhat dubiously, take the
+  // this opportunity to make it do something more useful.
+  containerClass.prototype.add = function (actor, props) {
+    this.add_actor(actor);
+    if (props) this.child_set(actor, props);
+  };
 }
 
 function _patchLayoutClass(layoutClass, styleProps) {
-    if (styleProps) {
-        layoutClass.prototype.hookup_style = function (container) {
-            container.connect('style-changed', () => {
-                let node = container.get_theme_node();
-                for (let prop in styleProps) {
-                    let [found, length] = node.lookup_length(styleProps[prop], false);
-                    if (found)
-                        this[prop] = length;
-                }
-            });
-        };
-    }
+  if (styleProps) {
+    layoutClass.prototype.hookup_style = function (container) {
+      container.connect("style-changed", () => {
+        let node = container.get_theme_node();
+        for (let prop in styleProps) {
+          let [found, length] = node.lookup_length(styleProps[prop], false);
+          if (found) this[prop] = length;
+        }
+      });
+    };
+  }
 }
 
 /**
@@ -116,331 +119,350 @@ function _injectSoup3Compat(Soup) {
 }
 
 function _makeEaseCallback(params, cleanup) {
-    let onComplete = params.onComplete;
-    delete params.onComplete;
+  let onComplete = params.onComplete;
+  delete params.onComplete;
 
-    let onStopped = params.onStopped;
-    delete params.onStopped;
+  let onStopped = params.onStopped;
+  delete params.onStopped;
 
-    return isFinished => {
-        cleanup();
+  return (isFinished) => {
+    cleanup();
 
-        if (onStopped)
-            onStopped(isFinished);
-        if (onComplete && isFinished)
-            onComplete();
-    };
+    if (onStopped) onStopped(isFinished);
+    if (onComplete && isFinished) onComplete();
+  };
 }
 
 function _getPropertyTarget(actor, propName) {
-    if (!propName.startsWith('@'))
-        return [actor, propName];
-
-    let [type, name, prop] = propName.split('.');
-    switch (type) {
-    case '@layout':
-        return [actor.layout_manager, name];
-    case '@actions':
-        return [actor.get_action(name), prop];
-    case '@constraints':
-        return [actor.get_constraint(name), prop];
-    case '@content':
-        return [actor.content, name];
-    case '@effects':
-        return [actor.get_effect(name), prop];
-    }
-
-    throw new Error(`Invalid property name ${propName}`);
+  if (!propName.startsWith("@")) return [actor, propName];
+
+  let [type, name, prop] = propName.split(".");
+  switch (type) {
+    case "@layout":
+      return [actor.layout_manager, name];
+    case "@actions":
+      return [actor.get_action(name), prop];
+    case "@constraints":
+      return [actor.get_constraint(name), prop];
+    case "@content":
+      return [actor.content, name];
+    case "@effects":
+      return [actor.get_effect(name), prop];
+  }
+
+  throw new Error(`Invalid property name ${propName}`);
 }
 
 function _easeActor(actor, params) {
-    actor.save_easing_state();
-
-    if (params.duration != undefined)
-        actor.set_easing_duration(params.duration);
-    delete params.duration;
-
-    if (params.delay != undefined)
-        actor.set_easing_delay(params.delay);
-    delete params.delay;
-
-    let repeatCount = 0;
-    if (params.repeatCount != undefined)
-        repeatCount = params.repeatCount;
-    delete params.repeatCount;
-
-    let autoReverse = false;
-    if (params.autoReverse != undefined)
-        autoReverse = params.autoReverse;
-    delete params.autoReverse;
-
-    // repeatCount doesn't include the initial iteration
-    const numIterations = repeatCount + 1;
-    // whether the transition should finish where it started
-    const isReversed = autoReverse && numIterations % 2 === 0;
-
-    if (params.mode != undefined)
-        actor.set_easing_mode(params.mode);
-    delete params.mode;
-
-    const prepare = () => {
-        Meta.disable_unredirect_for_display(global.display);
-        global.begin_work();
-    };
-    const cleanup = () => {
-        Meta.enable_unredirect_for_display(global.display);
-        global.end_work();
-    };
-    let callback = _makeEaseCallback(params, cleanup);
-
-    // cancel overwritten transitions
-    let animatedProps = Object.keys(params).map(p => p.replace('_', '-', 'g'));
-    animatedProps.forEach(p => actor.remove_transition(p));
-
-    if (actor.get_easing_duration() > 0 || !isReversed)
-        actor.set(params);
-    actor.restore_easing_state();
-
-    let transition = animatedProps.map(p => actor.get_transition(p))
-        .find(t => t !== null);
-
-    if (transition && transition.delay)
-        transition.connect('started', () => prepare());
-    else
-        prepare();
-
-    if (transition) {
-        transition.set({ repeatCount, autoReverse });
-        transition.connect('stopped', (t, finished) => callback(finished));
-    } else {
-        callback(true);
-    }
+  actor.save_easing_state();
+
+  if (params.duration != undefined) actor.set_easing_duration(params.duration);
+  delete params.duration;
+
+  if (params.delay != undefined) actor.set_easing_delay(params.delay);
+  delete params.delay;
+
+  let repeatCount = 0;
+  if (params.repeatCount != undefined) repeatCount = params.repeatCount;
+  delete params.repeatCount;
+
+  let autoReverse = false;
+  if (params.autoReverse != undefined) autoReverse = params.autoReverse;
+  delete params.autoReverse;
+
+  // repeatCount doesn't include the initial iteration
+  const numIterations = repeatCount + 1;
+  // whether the transition should finish where it started
+  const isReversed = autoReverse && numIterations % 2 === 0;
+
+  if (params.mode != undefined) actor.set_easing_mode(params.mode);
+  delete params.mode;
+
+  const prepare = () => {
+    Meta.disable_unredirect_for_display(global.display);
+    global.begin_work();
+  };
+  const cleanup = () => {
+    Meta.enable_unredirect_for_display(global.display);
+    global.end_work();
+  };
+  let callback = _makeEaseCallback(params, cleanup);
+
+  // cancel overwritten transitions
+  let animatedProps = Object.keys(params).map((p) => p.replace(/_/g, "-"));
+  animatedProps.forEach((p) => actor.remove_transition(p));
+
+  if (actor.get_easing_duration() > 0 || !isReversed) actor.set(params);
+  actor.restore_easing_state();
+
+  let transition = animatedProps.map((p) => actor.get_transition(p)).find((t) => t !== null);
+
+  if (transition && transition.delay) transition.connect("started", () => prepare());
+  else prepare();
+
+  if (transition) {
+    transition.set({ repeatCount, autoReverse });
+    transition.connect("stopped", (t, finished) => callback(finished));
+  } else {
+    callback(true);
+  }
 }
 
 function _easeActorProperty(actor, propName, target, params) {
-    // Avoid pointless difference with ease()
-    if (params.mode)
-        params.progress_mode = params.mode;
-    delete params.mode;
-
-    if (params.duration)
-        params.duration = adjustAnimationTime(params.duration);
-    let duration = Math.floor(params.duration || 0);
-
-    let repeatCount = 0;
-    if (params.repeatCount != undefined)
-        repeatCount = params.repeatCount;
-    delete params.repeatCount;
-
-    let autoReverse = false;
-    if (params.autoReverse != undefined)
-        autoReverse = params.autoReverse;
-    delete params.autoReverse;
-
-    // repeatCount doesn't include the initial iteration
-    const numIterations = repeatCount + 1;
-    // whether the transition should finish where it started
-    const isReversed = autoReverse && numIterations % 2 === 0;
-
-    // Copy Clutter's behavior for implicit animations, see
-    // should_skip_implicit_transition()
-    if (actor instanceof Clutter.Actor && !actor.mapped)
-        duration = 0;
-
-    const prepare = () => {
-        Meta.disable_unredirect_for_display(global.display);
-        global.begin_work();
-    };
-    const cleanup = () => {
-        Meta.enable_unredirect_for_display(global.display);
-        global.end_work();
-    };
-    let callback = _makeEaseCallback(params, cleanup);
-
-    // cancel overwritten transition
-    actor.remove_transition(propName);
-
-    if (duration == 0) {
-        let [obj, prop] = _getPropertyTarget(actor, propName);
-
-        if (!isReversed)
-            obj[prop] = target;
-
-        prepare();
-        callback(true);
-
-        return;
-    }
-
-    let pspec = actor.find_property(propName);
-    let transition = new Clutter.PropertyTransition(Object.assign({
+  // Avoid pointless difference with ease()
+  if (params.mode) params.progress_mode = params.mode;
+  delete params.mode;
+
+  if (params.duration) params.duration = adjustAnimationTime(params.duration);
+  let duration = Math.floor(params.duration || 0);
+
+  let repeatCount = 0;
+  if (params.repeatCount != undefined) repeatCount = params.repeatCount;
+  delete params.repeatCount;
+
+  let autoReverse = false;
+  if (params.autoReverse != undefined) autoReverse = params.autoReverse;
+  delete params.autoReverse;
+
+  // repeatCount doesn't include the initial iteration
+  const numIterations = repeatCount + 1;
+  // whether the transition should finish where it started
+  const isReversed = autoReverse && numIterations % 2 === 0;
+
+  // Copy Clutter's behavior for implicit animations, see
+  // should_skip_implicit_transition()
+  if (actor instanceof Clutter.Actor && !actor.mapped) duration = 0;
+
+  const prepare = () => {
+    Meta.disable_unredirect_for_display(global.display);
+    global.begin_work();
+  };
+  const cleanup = () => {
+    Meta.enable_unredirect_for_display(global.display);
+    global.end_work();
+  };
+  let callback = _makeEaseCallback(params, cleanup);
+
+  // cancel overwritten transition
+  actor.remove_transition(propName);
+
+  if (duration == 0) {
+    let [obj, prop] = _getPropertyTarget(actor, propName);
+
+    if (!isReversed) obj[prop] = target;
+
+    prepare();
+    callback(true);
+
+    return;
+  }
+
+  let pspec = actor.find_property(propName);
+  let transition = new Clutter.PropertyTransition(
+    Object.assign(
+      {
         property_name: propName,
         interval: new Clutter.Interval({ value_type: pspec.value_type }),
         remove_on_complete: true,
         repeat_count: repeatCount,
         auto_reverse: autoReverse,
-    }, params));
-    actor.add_transition(propName, transition);
+      },
+      params
+    )
+  );
+  actor.add_transition(propName, transition);
 
-    transition.set_to(target);
+  transition.set_to(target);
 
-    if (transition.delay)
-        transition.connect('started', () => prepare());
-    else
-        prepare();
+  if (transition.delay) transition.connect("started", () => prepare());
+  else prepare();
 
-    transition.connect('stopped', (t, finished) => callback(finished));
+  transition.connect("stopped", (t, finished) => callback(finished));
 }
 
 function _loggingFunc(...args) {
-    let fields = { 'MESSAGE': args.join(', ') };
-    let domain = "GNOME Shell";
-
-    // If the caller is an extension, add it as metadata
-    let extension = imports.misc.extensionUtils.getCurrentExtension();
-    if (extension != null) {
-        domain = extension.metadata.name;
-        fields['GNOME_SHELL_EXTENSION_UUID'] = extension.uuid;
-        fields['GNOME_SHELL_EXTENSION_NAME'] = extension.metadata.name;
-    }
-
-    GLib.log_structured(domain, GLib.LogLevelFlags.LEVEL_MESSAGE, fields);
+  let fields = { MESSAGE: args.join(", ") };
+  let domain = "GNOME Shell";
+
+  // If the caller is an extension, add it as metadata
+  // let extension = ExtensionUtils.getCurrentExtension();
+  // if (extension != null) {
+  //     domain = extension.metadata.name;
+  //     fields['GNOME_SHELL_EXTENSION_UUID'] = extension.uuid;
+  //     fields['GNOME_SHELL_EXTENSION_NAME'] = extension.metadata.name;
+  // }
+
+  GLib.log_structured(domain, GLib.LogLevelFlags.LEVEL_MESSAGE, fields);
 }
 
-function init() {
-    // Add some bindings to the global JS namespace
-    globalThis.global = Shell.Global.get();
-
-    globalThis.log = _loggingFunc;
-
-    globalThis._ = Gettext.gettext;
-    globalThis.C_ = Gettext.pgettext;
-    globalThis.ngettext = Gettext.ngettext;
-    globalThis.N_ = s => s;
-
-    GObject.gtypeNameBasedOnJSPath = true;
-
-    // Miscellaneous monkeypatching
-    _patchContainerClass(St.BoxLayout);
-
-    _patchLayoutClass(Clutter.GridLayout, { row_spacing: 'spacing-rows',
-                                            column_spacing: 'spacing-columns' });
-    _patchLayoutClass(Clutter.BoxLayout, { spacing: 'spacing' });
-
-    let origSetEasingDuration = Clutter.Actor.prototype.set_easing_duration;
-    Clutter.Actor.prototype.set_easing_duration = function (msecs) {
-        origSetEasingDuration.call(this, adjustAnimationTime(msecs));
-    };
-    let origSetEasingDelay = Clutter.Actor.prototype.set_easing_delay;
-    Clutter.Actor.prototype.set_easing_delay = function (msecs) {
-        origSetEasingDelay.call(this, adjustAnimationTime(msecs));
-    };
-
-    Clutter.Actor.prototype.ease = function (props) {
-        _easeActor(this, props);
-    };
-    Clutter.Actor.prototype.ease_property = function (propName, target, params) {
-        _easeActorProperty(this, propName, target, params);
-    };
-    St.Adjustment.prototype.ease = function (target, params) {
-        // we're not an actor of course, but we implement the same
-        // transition API as Clutter.Actor, so this works anyway
-        _easeActorProperty(this, 'value', target, params);
-    };
-
-    Clutter.Actor.prototype[Symbol.iterator] = function* () {
-        for (let c = this.get_first_child(); c; c = c.get_next_sibling())
-            yield c;
-    };
-
-    Clutter.Actor.prototype.toString = function () {
-        return St.describe_actor(this);
-    };
-    // Deprecation warning for former JS classes turned into an actor subclass
-    Object.defineProperty(Clutter.Actor.prototype, 'actor', {
-        get() {
-            let klass = this.constructor.name;
-            let { stack } = new Error();
-            log(`Usage of object.actor is deprecated for ${klass}\n${stack}`);
-            return this;
-        },
-    });
-
-    Gio._LocalFilePrototype.touch_async = function (callback) {
-        Shell.util_touch_file_async(this, callback);
-    };
-    Gio._LocalFilePrototype.touch_finish = function (result) {
-        return Shell.util_touch_file_finish(this, result);
-    };
-
-    St.set_slow_down_factor = function (factor) {
-        let { stack } = new Error();
-        log(`St.set_slow_down_factor() is deprecated, use St.Settings.slow_down_factor\n${stack}`);
-        St.Settings.get().slow_down_factor = factor;
-    };
+log("initializing...");
+// Add some bindings to the global JS namespace
+
+// TODO: This errors because the Shell global has an incorrect type for 'stage'
+/** @type {Shell.Global & { stage: import('gi://Clutter').Stage; }} */
+globalThis.global = (Shell.Global.get());
+
+globalThis.log = _loggingFunc;
+
+globalThis._ = Gettext.gettext;
+globalThis.C_ = Gettext.pgettext;
+globalThis.ngettext = Gettext.ngettext;
+globalThis.N_ = (s) => s;
+
+globalThis.assertType =
+  /**
+   * @template T
+   * @param {unknown} obj
+   * @param {new(...args: any[]) => T} type
+   * @param {string} [message]
+   * @returns {asserts obj is T}
+   */
+  (obj, type, message) => {
+    if (!(obj instanceof type)) {
+      throw new Error(`${obj} is not an instance of ${type.name}${message ? `: ${message}` : ""}`);
+    }
+  };
+
+GObject.gtypeNameBasedOnJSPath = true;
+
+// Miscellaneous monkeypatching
+_patchContainerClass(St.BoxLayout);
+
+_patchLayoutClass(Clutter.GridLayout, { row_spacing: "spacing-rows", column_spacing: "spacing-columns" });
+_patchLayoutClass(Clutter.BoxLayout, { spacing: "spacing" });
+
+let origSetEasingDuration = Clutter.Actor.prototype.set_easing_duration;
+Clutter.Actor.prototype.set_easing_duration = function (msecs) {
+  origSetEasingDuration.call(this, adjustAnimationTime(msecs));
+};
+let origSetEasingDelay = Clutter.Actor.prototype.set_easing_delay;
+Clutter.Actor.prototype.set_easing_delay = function (msecs) {
+  origSetEasingDelay.call(this, adjustAnimationTime(msecs));
+};
+
+Clutter.Actor.prototype.ease = function (props) {
+  _easeActor(this, props);
+};
+Clutter.Actor.prototype.ease_property = function (propName, target, params) {
+  _easeActorProperty(this, propName, target, params);
+};
+St.Adjustment.prototype.ease = function (target, params) {
+  // we're not an actor of course, but we implement the same
+  // transition API as Clutter.Actor, so this works anyway
+  _easeActorProperty(this, "value", target, params);
+};
+
+Clutter.Actor.prototype[Symbol.iterator] = function* () {
+  for (let c = this.get_first_child(); c; c = c.get_next_sibling()) yield c;
+};
+
+Clutter.Actor.prototype.toString = function () {
+  return St.describe_actor(this);
+};
+// Deprecation warning for former JS classes turned into an actor subclass
+Object.defineProperty(Clutter.Actor.prototype, "actor", {
+  get() {
+    let klass = this.constructor.name;
+    let { stack } = new Error();
+    log(`Usage of object.actor is deprecated for ${klass}\n${stack}`);
+    return this;
+  },
+});
+
+Gio._LocalFilePrototype.touch_async = function (callback) {
+  // TODO
+  if (!callback) {
+    return Shell.util_touch_file_async(this);
+  }
+
+  Shell.util_touch_file_async(this, callback);
+};
+
+Gio._LocalFilePrototype.touch_finish =
+  /**
+   * @this {Gio.File}
+   * @param {Gio.AsyncResult} result
+   */
+  function (result) {
+    return Shell.util_touch_file_finish(this, result);
+  };
+
+St.set_slow_down_factor = function (factor) {
+  let { stack } = new Error();
+  log(`St.set_slow_down_factor() is deprecated, use St.Settings.slow_down_factor\n${stack}`);
+  St.Settings.get().slow_down_factor = factor;
+};
 
-    let origToString = Object.prototype.toString;
-    Object.prototype.toString = function () {
-        let base = origToString.call(this);
-        try {
-            if ('actor' in this && this.actor instanceof Clutter.Actor)
-                return base.replace(/\]$/, ` delegate for ${this.actor.toString().substring(1)}`);
-            else
-                return base;
-        } catch (e) {
-            return base;
-        }
-    };
+/**
+ * @param {unknown} obj
+ * @returns {obj is {actor: unknown}}
+ */
+function hasActor(obj) {
+  let actorObj = /** @type {{actor?: unknown}} */ (obj);
 
-    // Override to clear our own timezone cache as well
-    const origClearDateCaches = System.clearDateCaches;
-    System.clearDateCaches = function () {
-        _localTimeZone = null;
-        origClearDateCaches();
-    };
+  return "actor" in actorObj;
+}
 
-    // Work around https://bugzilla.mozilla.org/show_bug.cgi?id=508783
-    Date.prototype.toLocaleFormat = function (format) {
-        if (_localTimeZone === null)
-            _localTimeZone = GLib.TimeZone.new_local();
-
-        let dt = GLib.DateTime.new(_localTimeZone,
-            this.getFullYear(),
-            this.getMonth() + 1,
-            this.getDate(),
-            this.getHours(),
-            this.getMinutes(),
-            this.getSeconds());
-        return dt?.format(format) ?? '';
-    };
+let origToString = Object.prototype.toString;
+Object.prototype.toString = function () {
+  let base = origToString.call(this);
+  try {
+    if (hasActor(this) && this.actor instanceof Clutter.Actor)
+      return base.replace(/\]$/, ` delegate for ${this.actor.toString().substring(1)}`);
+    else return base;
+  } catch (e) {
+    return base;
+  }
+};
+
+// Override to clear our own timezone cache as well
+const origClearDateCaches = System.clearDateCaches;
+System.clearDateCaches = function () {
+  _localTimeZone = null;
+  origClearDateCaches();
+};
+
+// Work around https://bugzilla.mozilla.org/show_bug.cgi?id=508783
+Date.prototype.toLocaleFormat = function (format) {
+  if (_localTimeZone === null) _localTimeZone = GLib.TimeZone.new_local();
+
+  let dt = GLib.DateTime.new(
+    _localTimeZone,
+    this.getFullYear(),
+    this.getMonth() + 1,
+    this.getDate(),
+    this.getHours(),
+    this.getMinutes(),
+    this.getSeconds()
+  );
+  return dt?.format(format) ?? "";
+};
+
+let slowdownEnv = GLib.getenv("GNOME_SHELL_SLOWDOWN_FACTOR");
+if (slowdownEnv) {
+  let factor = parseFloat(slowdownEnv);
+  if (!isNaN(factor) && factor > 0.0) St.Settings.get().slow_down_factor = factor;
+}
 
-    let slowdownEnv = GLib.getenv('GNOME_SHELL_SLOWDOWN_FACTOR');
-    if (slowdownEnv) {
-        let factor = parseFloat(slowdownEnv);
-        if (!isNaN(factor) && factor > 0.0)
-            St.Settings.get().slow_down_factor = factor;
-    }
+// OK, now things are initialized enough that we can import shell JS
+const Format = imports.format;
 
-    // OK, now things are initialized enough that we can import shell JS
-    const Format = imports.format;
+String.prototype.format = Format.format;
 
-    String.prototype.format = Format.format;
+Math.clamp = function (x, lower, upper) {
+  return Math.min(Math.max(x, lower), upper);
+};
 
-    Math.clamp = function (x, lower, upper) {
-        return Math.min(Math.max(x, lower), upper);
-    };
-}
+log("done initializing...");
 
 // adjustAnimationTime:
 // @msecs: time in milliseconds
 //
 // Adjust @msecs to account for St's enable-animations
 // and slow-down-factor settings
-function adjustAnimationTime(msecs) {
-    let settings = St.Settings.get();
+export function adjustAnimationTime(msecs) {
+  let settings = St.Settings.get();
 
-    if (!settings.enable_animations)
-        return 1;
-    return settings.slow_down_factor * msecs;
+  if (!settings.enable_animations) return 1;
+  return settings.slow_down_factor * msecs;
 }
-
diff --git a/js/ui/extensionDownloader.js b/js/ui/extensionDownloader.js
index 8bf4646a6a..8a6e51633b 100644
--- a/js/ui/extensionDownloader.js
+++ b/js/ui/extensionDownloader.js
@@ -1,14 +1,18 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported init, installExtension, uninstallExtension, checkForUpdates */
 
-const { Clutter, Gio, GLib, GObject, Soup } = imports.gi;
+import Clutter from 'gi://Clutter';
+import Gio from 'gi://Gio';
+import GLib from 'gi://GLib';
+import GObject from 'gi://GObject';
+import Soup from 'gi://Soup';
 
 const Config = imports.misc.config;
-const Dialog = imports.ui.dialog;
-const ExtensionUtils = imports.misc.extensionUtils;
-const FileUtils = imports.misc.fileUtils;
-const Main = imports.ui.main;
-const ModalDialog = imports.ui.modalDialog;
+import * as Dialog from './dialog.js';
+import * as ExtensionUtils from '../misc/extensionUtils.js';
+import * as FileUtils from '../misc/fileUtilsModule.js';
+import Main from './main.js';
+import * as ModalDialog from './modalDialog.js';
 
 Gio._promisify(Soup.Session.prototype,
     'send_and_read_async', 'send_and_read_finish');
@@ -19,10 +23,10 @@ Gio._promisify(Gio.IOStream.prototype,
 Gio._promisify(Gio.Subprocess.prototype,
     'wait_check_async', 'wait_check_finish');
 
-var REPOSITORY_URL_DOWNLOAD = 'https://extensions.gnome.org/download-extension/%s.shell-extension.zip';
-var REPOSITORY_URL_INFO     = 'https://extensions.gnome.org/extension-info/';
-var REPOSITORY_URL_UPDATE   = 'https://extensions.gnome.org/update-info/';
-
+export const REPOSITORY_URL_DOWNLOAD = 
'https://extensions.gnome.org/download-extension/%s.shell-extension.zip';
+export const REPOSITORY_URL_INFO     = 'https://extensions.gnome.org/extension-info/';
+export const REPOSITORY_URL_UPDATE   = 'https://extensions.gnome.org/update-info/';
+    
 let _httpSession;
 
 /**
@@ -60,7 +64,7 @@ async function installExtension(uuid, invocation) {
     dialog.open(global.get_current_time());
 }
 
-function uninstallExtension(uuid) {
+export function uninstallExtension(uuid) {
     let extension = Main.extensionManager.lookup(uuid);
     if (!extension)
         return false;
@@ -215,7 +219,7 @@ async function checkForUpdates() {
     }
 }
 
-var InstallExtensionDialog = GObject.registerClass(
+export const InstallExtensionDialog = GObject.registerClass(
 class InstallExtensionDialog extends ModalDialog.ModalDialog {
     _init(uuid, info, invocation) {
         super._init({ styleClass: 'extension-dialog' });
diff --git a/js/ui/extensionSystem.js b/js/ui/extensionSystem.js
index 3c1508d77b..21baa4ab5c 100644
--- a/js/ui/extensionSystem.js
+++ b/js/ui/extensionSystem.js
@@ -1,14 +1,18 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported init connect disconnect ExtensionManager */
 
-const { GLib, Gio, GObject, Shell, St } = imports.gi;
-const Signals = imports.misc.signals;
-
-const ExtensionDownloader = imports.ui.extensionDownloader;
-const ExtensionUtils = imports.misc.extensionUtils;
-const FileUtils = imports.misc.fileUtils;
-const Main = imports.ui.main;
-const MessageTray = imports.ui.messageTray;
+import GLib from 'gi://GLib';
+import Gio from 'gi://Gio';
+import GObject from 'gi://GObject';
+import Shell from 'gi://Shell';
+import St from 'gi://St';
+import * as Signals from '../misc/signals.js';
+
+import * as ExtensionDownloader from './extensionDownloader.js';
+import * as ExtensionUtils from '../misc/extensionUtils.js';
+import * as FileUtils from '../misc/fileUtilsModule.js';
+import Main from './main.js';
+import * as MessageTray from './messageTray.js';
 
 const { ExtensionState, ExtensionType } = ExtensionUtils;
 
@@ -19,7 +23,7 @@ const EXTENSION_DISABLE_VERSION_CHECK_KEY = 'disable-extension-version-validatio
 
 const UPDATE_CHECK_TIMEOUT = 24 * 60 * 60; // 1 day in seconds
 
-var ExtensionManager = class extends Signals.EventEmitter {
+export class ExtensionManager extends Signals.EventEmitter {
     constructor() {
         super();
 
@@ -418,35 +422,36 @@ var ExtensionManager = class extends Signals.EventEmitter {
         let extensionModule;
         let extensionState = null;
 
-        ExtensionUtils.installImporter(extension);
+        // TODO: Fix extension initialization.
+        // ExtensionUtils.installImporter(extension);
         try {
-            extensionModule = extension.imports.extension;
+        //    extensionModule = extension.imports.extension;
         } catch (e) {
             this.logExtensionError(uuid, e);
             return false;
         }
 
-        if (extensionModule.init) {
-            try {
-                extensionState = extensionModule.init(extension);
-            } catch (e) {
-                this.logExtensionError(uuid, e);
-                return false;
-            }
-        }
+        // if (extensionModule.init) {
+        //     try {
+        //         extensionState = extensionModule.init(extension);
+        //     } catch (e) {
+        //         this.logExtensionError(uuid, e);
+        //         return false;
+        //     }
+        // }
 
-        if (!extensionState)
-            extensionState = extensionModule;
-        extension.stateObj = extensionState;
+        // if (!extensionState)
+        //     extensionState = extensionModule;
+        // extension.stateObj = extensionState;
 
-        extension.state = ExtensionState.DISABLED;
-        this.emit('extension-loaded', uuid);
-        return true;
+        // extension.state = ExtensionState.DISABLED;
+        // this.emit('extension-loaded', uuid);
+        return false;
     }
 
     _getModeExtensions() {
-        if (Array.isArray(Main.sessionMode.enabledExtensions))
-            return Main.sessionMode.enabledExtensions;
+        // if (Array.isArray(Main.sessionMode.enabledExtensions))
+        //     return Main.sessionMode.enabledExtensions;
         return [];
     }
 
@@ -499,7 +504,7 @@ var ExtensionManager = class extends Signals.EventEmitter {
             .filter(uuid => !newEnabledExtensions.includes(uuid))
             .reverse().forEach(uuid => this._callExtensionDisable(uuid));
 
-        this._enabledExtensions = newEnabledExtensions;
+        this._enabledExtensions = []; //newEnabledExtensions;
     }
 
     _onSettingsWritableChanged() {
@@ -625,10 +630,10 @@ var ExtensionManager = class extends Signals.EventEmitter {
         // from allowExtensions in the future
         if (Main.sessionMode.allowExtensions) {
             // Take care of added or removed sessionMode extensions
-            this._onEnabledExtensionsChanged();
-            this._enableAllExtensions();
+            // this._onEnabledExtensionsChanged();
+            // this._enableAllExtensions();
         } else {
-            this._disableAllExtensions();
+            // this._disableAllExtensions();
         }
     }
 };
@@ -639,7 +644,7 @@ class ExtensionUpdateSource extends MessageTray.Source {
         let appSys = Shell.AppSystem.get_default();
         this._app = appSys.lookup_app('org.gnome.Extensions.desktop');
 
-        super._init(this._app.get_name());
+        super._init({ title: this._app.get_name() });
     }
 
     getIcon() {
diff --git a/js/ui/focusCaretTracker.js b/js/ui/focusCaretTracker.js
index 5cfe7a8496..456fa7dd0c 100644
--- a/js/ui/focusCaretTracker.js
+++ b/js/ui/focusCaretTracker.js
@@ -22,13 +22,13 @@
  */
 /* exported FocusCaretTracker */
 
-const Atspi = imports.gi.Atspi;
-const Signals = imports.misc.signals;
+import Atspi from 'gi://Atspi';
+import * as Signals from '../misc/signals.js';
 
 const CARETMOVED        = 'object:text-caret-moved';
 const STATECHANGED      = 'object:state-changed';
 
-var FocusCaretTracker = class FocusCaretTracker extends Signals.EventEmitter {
+export class FocusCaretTracker extends Signals.EventEmitter {
     constructor() {
         super();
 
diff --git a/js/ui/grabHelper.js b/js/ui/grabHelper.js
index 72c1fec44d..84fdc8503b 100644
--- a/js/ui/grabHelper.js
+++ b/js/ui/grabHelper.js
@@ -1,9 +1,11 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported GrabHelper */
 
-const { Clutter, St } = imports.gi;
+import Clutter from 'gi://Clutter';
+import St from 'gi://St';
 
-const Main = imports.ui.main;
+
+import Main from './main.js';
 
 let _capturedEventId = 0;
 let _grabHelperStack = [];
@@ -30,6 +32,8 @@ function _popGrabHelper(grabHelper) {
     }
 }
 
+/** @typedef {{ actor: St.Widget, focus?: St.Widget, savedFocus?: Clutter.Actor, onUngrab: (isUser: boolean) 
=> void }} Grab */
+
 // GrabHelper:
 // @owner: the actor that owns the GrabHelper
 // @params: optional parameters to pass to Main.pushModal()
@@ -41,7 +45,7 @@ function _popGrabHelper(grabHelper) {
 // your code just needs to deal with it; you shouldn't adjust behavior directly
 // after you call ungrab(), but instead pass an 'onUngrab' callback when you
 // call grab().
-var GrabHelper = class GrabHelper {
+export class GrabHelper {
     constructor(owner, params) {
         if (!(owner instanceof Clutter.Actor))
             throw new Error('GrabHelper owner must be a Clutter.Actor');
diff --git a/js/ui/ibusCandidatePopup.js b/js/ui/ibusCandidatePopup.js
index 5a9fe77928..129e2c5fd8 100644
--- a/js/ui/ibusCandidatePopup.js
+++ b/js/ui/ibusCandidatePopup.js
@@ -1,17 +1,21 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported CandidatePopup */
 
-const { Clutter, GObject, IBus, St } = imports.gi;
+import Clutter from 'gi://Clutter';
+import GObject from 'gi://GObject';
+import IBus from 'gi://IBus';
+import St from 'gi://St';
 
-const BoxPointer = imports.ui.boxpointer;
-const Main = imports.ui.main;
 
-var MAX_CANDIDATES_PER_PAGE = 16;
+import * as BoxPointer from './boxpointer.js';
+import Main from './main.js';
 
-var DEFAULT_INDEX_LABELS = ['1', '2', '3', '4', '5', '6', '7', '8',
+export let MAX_CANDIDATES_PER_PAGE = 16;
+
+export let DEFAULT_INDEX_LABELS = ['1', '2', '3', '4', '5', '6', '7', '8',
                             '9', '0', 'a', 'b', 'c', 'd', 'e', 'f'];
 
-var CandidateArea = GObject.registerClass({
+export const CandidateArea = GObject.registerClass({
     Signals: {
         'candidate-clicked': { param_types: [GObject.TYPE_UINT,
                                              GObject.TYPE_UINT,
@@ -98,12 +102,20 @@ var CandidateArea = GObject.registerClass({
             this.vertical = false;
             this.remove_style_class_name('vertical');
             this.add_style_class_name('horizontal');
+
+            assertType(this._previousButton.child, St.Icon);
+            assertType(this._nextButton.child, St.Icon);
+
             this._previousButton.child.icon_name = 'go-previous-symbolic';
             this._nextButton.child.icon_name = 'go-next-symbolic';
         } else {                // VERTICAL || SYSTEM
             this.vertical = true;
             this.add_style_class_name('vertical');
             this.remove_style_class_name('horizontal');
+
+            assertType(this._previousButton.child, St.Icon);
+            assertType(this._nextButton.child, St.Icon);
+
             this._previousButton.child.icon_name = 'go-up-symbolic';
             this._nextButton.child.icon_name = 'go-down-symbolic';
         }
@@ -139,7 +151,7 @@ var CandidateArea = GObject.registerClass({
     }
 });
 
-var CandidatePopup = GObject.registerClass(
+export const CandidatePopup = GObject.registerClass(
 class IbusCandidatePopup extends BoxPointer.BoxPointer {
     _init() {
         super._init(St.Side.TOP);
@@ -199,6 +211,7 @@ class IbusCandidatePopup extends BoxPointer.BoxPointer {
             panelService.connect('set-cursor-location-relative', (ps, x, y, w, h) => {
                 if (!global.display.focus_window)
                     return;
+
                 let window = global.display.focus_window.get_compositor_private();
                 this._setDummyCursorGeometry(window.x + x, window.y + y, w, h);
             });
diff --git a/js/ui/iconGrid.js b/js/ui/iconGrid.js
index 4f011fe67f..1c84163cdb 100644
--- a/js/ui/iconGrid.js
+++ b/js/ui/iconGrid.js
@@ -1,37 +1,49 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported BaseIcon, IconGrid, IconGridLayout */
 
-const { Clutter, GLib, GObject, Meta, Shell, St } = imports.gi;
+import Clutter from 'gi://Clutter';
+import GLib from 'gi://GLib';
+import GObject from 'gi://GObject';
+import Meta from 'gi://Meta';
+import Shell from 'gi://Shell';
+import St from 'gi://St';
 
-const Main = imports.ui.main;
+import Main from './main.js';
 
-var ICON_SIZE = 96;
+export let ICON_SIZE = 96;
 
-var ANIMATION_TIME_IN = 350;
-var ANIMATION_TIME_OUT = 1 / 2 * ANIMATION_TIME_IN;
-var ANIMATION_MAX_DELAY_FOR_ITEM = 2 / 3 * ANIMATION_TIME_IN;
-var ANIMATION_MAX_DELAY_OUT_FOR_ITEM = 2 / 3 * ANIMATION_TIME_OUT;
-var ANIMATION_FADE_IN_TIME_FOR_ITEM = 1 / 4 * ANIMATION_TIME_IN;
+export let ANIMATION_TIME_IN = 350;
+export let ANIMATION_TIME_OUT = 1 / 2 * ANIMATION_TIME_IN;
+export let ANIMATION_MAX_DELAY_FOR_ITEM = 2 / 3 * ANIMATION_TIME_IN;
+export let ANIMATION_MAX_DELAY_OUT_FOR_ITEM = 2 / 3 * ANIMATION_TIME_OUT;
+export let ANIMATION_FADE_IN_TIME_FOR_ITEM = 1 / 4 * ANIMATION_TIME_IN;
 
-var PAGE_SWITCH_TIME = 300;
+export let PAGE_SWITCH_TIME = 300;
 
-var AnimationDirection = {
+/** @typedef {Clutter.Actor & { icon?: typeof BaseIcon["prototype"] }} BaseItem */
+
+/** @enum {number} */
+export const AnimationDirection = {
     IN: 0,
     OUT: 1,
 };
 
-var IconSize = {
+/** @enum {number} */
+export const IconSize = {
     LARGE: 96,
     MEDIUM: 64,
     SMALL: 32,
     TINY: 16,
 };
 
-var APPICON_ANIMATION_OUT_SCALE = 3;
-var APPICON_ANIMATION_OUT_TIME = 250;
+export let APPICON_ANIMATION_OUT_SCALE = 3;
+export let APPICON_ANIMATION_OUT_TIME = 250;
 
 const ICON_POSITION_DELAY = 10;
 
+/** @typedef {{rows: number, columns: number}} GridMode */
+
+/** @type {GridMode[]} */
 const defaultGridModes = [
     {
         rows: 8,
@@ -51,10 +63,11 @@ const defaultGridModes = [
     },
 ];
 
-var LEFT_DIVIDER_LEEWAY = 20;
-var RIGHT_DIVIDER_LEEWAY = 20;
+export let LEFT_DIVIDER_LEEWAY = 20;
+export let RIGHT_DIVIDER_LEEWAY = 20;
 
-var DragLocation = {
+/** @enum {number} */
+export const DragLocation = {
     INVALID: 0,
     START_EDGE: 1,
     ON_ICON: 2,
@@ -62,7 +75,7 @@ var DragLocation = {
     EMPTY_SPACE: 4,
 };
 
-var BaseIcon = GObject.registerClass(
+export const BaseIcon = GObject.registerClass(
 class BaseIcon extends Shell.SquareBin {
     _init(label, params = {}) {
         const {
@@ -114,6 +127,10 @@ class BaseIcon extends Shell.SquareBin {
 
     // This can be overridden by a subclass, or by the createIcon
     // parameter to _init()
+    /**
+     * @param {number} _size 
+     * @returns {St.Widget}
+     */
     createIcon(_size) {
         throw new GObject.NotImplementedError(`createIcon in ${this.constructor.name}`);
     }
@@ -186,7 +203,7 @@ class BaseIcon extends Shell.SquareBin {
     }
 });
 
-function zoomOutActor(actor) {
+export function zoomOutActor(actor) {
     let [x, y] = actor.get_transformed_position();
     zoomOutActorAtPos(actor, x, y);
 }
@@ -250,7 +267,7 @@ function swap(value, length) {
     return length - value - 1;
 }
 
-var IconGridLayout = GObject.registerClass({
+export const IconGridLayout = GObject.registerClass({
     Properties: {
         'allow-incomplete-pages': GObject.ParamSpec.boolean('allow-incomplete-pages',
             'Allow incomplete pages', 'Allow incomplete pages',
@@ -319,6 +336,7 @@ var IconGridLayout = GObject.registerClass({
         'pages-changed': {},
     },
 }, class IconGridLayout extends Clutter.LayoutManager {
+
     _init(params = {}) {
         this._orientation = params.orientation ?? Clutter.Orientation.VERTICAL;
 
@@ -327,6 +345,29 @@ var IconGridLayout = GObject.registerClass({
         if (!this.pagePadding)
             this.pagePadding = new Clutter.Margin();
 
+        /** @type {number} */
+        this.fixedIconSize;
+        /** @type {number} */
+        this.rowSpacing;
+        /** @type {number} */
+        this.columnSpacing;
+        /** @type {number} */
+        this.maxRowSpacing;
+        /** @type {number} */
+        this.maxColumnSpacing;
+        /** @type {number} */
+        this.columnsPerPage;
+        /** @type {number} */
+        this.rowsPerPage;
+        /** @type {number} */
+        this.lastRowAlign;
+        /** @type {number} */
+        this.allowIncompletePages;
+        /** @type {Clutter.ActorAlign} */
+        this.pageHalign;
+        /** @type {Clutter.ActorAlign} */
+        this.pageValign;
+
         this._iconSize = this.fixedIconSize !== -1
             ? this.fixedIconSize
             : IconSize.LARGE;
@@ -729,6 +770,9 @@ var IconGridLayout = GObject.registerClass({
             this._containerDestroyedId = this._container.connect('destroy', this._onDestroy.bind(this));
     }
 
+    /**
+     * @returns {[number, number]} 
+     */
     vfunc_get_preferred_width(_container, _forHeight) {
         let minWidth = -1;
         let natWidth = -1;
@@ -748,6 +792,9 @@ var IconGridLayout = GObject.registerClass({
         return [minWidth, natWidth];
     }
 
+    /**
+     * @returns {[number, number]} 
+     */
     vfunc_get_preferred_height(_container, _forWidth) {
         let minHeight = -1;
         let natHeight = -1;
@@ -840,8 +887,8 @@ var IconGridLayout = GObject.registerClass({
     /**
      * addItem:
      * @param {Clutter.Actor} item: item to append to the grid
-     * @param {int} page: page number
-     * @param {int} index: position in the page
+     * @param {number} page: page number
+     * @param {number} index: position in the page
      *
      * Adds @item to the grid. @item must not be part of the grid.
      *
@@ -880,8 +927,8 @@ var IconGridLayout = GObject.registerClass({
     /**
      * moveItem:
      * @param {Clutter.Actor} item: item to move
-     * @param {int} newPage: new page of the item
-     * @param {int} newPosition: new page of the item
+     * @param {number} newPage: new page of the item
+     * @param {number} newPosition: new page of the item
      *
      * Moves @item to the grid. @item must be part of the grid.
      */
@@ -916,7 +963,7 @@ var IconGridLayout = GObject.registerClass({
 
     /**
      * getItemsAtPage:
-     * @param {int} pageIndex: page index
+     * @param {number} pageIndex: page index
      *
      * Retrieves the children at page @pageIndex. Children may be invisible.
      *
@@ -931,12 +978,12 @@ var IconGridLayout = GObject.registerClass({
 
     /**
      * getItemPosition:
-     * @param {BaseIcon} item: the item
+     * @param {BaseIcon["prototype"]} item: the item
      *
      * Retrieves the position of @item is its page, or -1 if @item is not
      * part of the grid.
      *
-     * @returns {[int, int]} the page and position of @item
+     * @returns {[number, number]} the page and position of @item
      */
     getItemPosition(item) {
         if (!this._items.has(item))
@@ -950,8 +997,8 @@ var IconGridLayout = GObject.registerClass({
 
     /**
      * getItemAt:
-     * @param {int} page: the page
-     * @param {int} position: the position in page
+     * @param {number} page: the page
+     * @param {number} position: the position in page
      *
      * Retrieves the item at @page and @position.
      *
@@ -971,11 +1018,11 @@ var IconGridLayout = GObject.registerClass({
 
     /**
      * getItemPage:
-     * @param {BaseIcon} item: the item
+     * @param {BaseIcon["prototype"]} item: the item
      *
      * Retrieves the page @item is in, or -1 if @item is not part of the grid.
      *
-     * @returns {int} the page where @item is in
+     * @returns {number} the page where @item is in
      */
     getItemPage(item) {
         if (!this._items.has(item))
@@ -1023,13 +1070,13 @@ var IconGridLayout = GObject.registerClass({
 
     /**
      * getDropTarget:
-     * @param {int} x: position of the horizontal axis
-     * @param {int} y: position of the vertical axis
+     * @param {number} x: position of the horizontal axis
+     * @param {number} y: position of the vertical axis
      *
      * Retrieves the item located at (@x, @y), as well as the drag location.
      * Both @x and @y are relative to the grid.
      *
-     * @returns {[Clutter.Actor, DragLocation]} the item and drag location
+     * @returns {[BaseIcon["prototype"] | null, DragLocation]} the item and drag location
      * under (@x, @y)
      */
     getDropTarget(x, y) {
@@ -1154,12 +1201,17 @@ var IconGridLayout = GObject.registerClass({
     }
 });
 
-var IconGrid = GObject.registerClass({
+export const IconGrid = GObject.registerClass({
     Signals: {
         'pages-changed': {},
         'animation-done': {},
     },
-}, class IconGrid extends St.Viewport {
+}, 
+/** @extends {St.Viewport<typeof IconGridLayout["prototype"]>} */
+class IconGrid extends St.Viewport {
+    /**
+     * @param {*} layoutParams 
+     */
     _init(layoutParams = {}) {
         const iconGridLayoutParams = {
             allow_incomplete_pages: false,
@@ -1238,6 +1290,9 @@ var IconGrid = GObject.registerClass({
         this.goToPage(itemPage);
     }
 
+    /**
+     * @param {number} modeIndex 
+     */
     _setGridMode(modeIndex) {
         if (this._currentMode === modeIndex)
             return;
@@ -1259,17 +1314,16 @@ var IconGrid = GObject.registerClass({
 
         const sizeRatio = width / height;
         let closestRatio = Infinity;
-        let bestMode = -1;
-
-        for (let modeIndex in this._gridModes) {
-            const mode = this._gridModes[modeIndex];
+        let bestMode = this._gridModes.findIndex((mode) => {
             const modeRatio = mode.columns / mode.rows;
 
             if (Math.abs(sizeRatio - modeRatio) < Math.abs(sizeRatio - closestRatio)) {
                 closestRatio = modeRatio;
-                bestMode = modeIndex;
+                return true;
             }
-        }
+
+            return false;
+        }) ?? -1;
 
         this._setGridMode(bestMode);
     }
@@ -1312,9 +1366,9 @@ var IconGrid = GObject.registerClass({
 
     /**
      * addItem:
-     * @param {Clutter.Actor} item: item to append to the grid
-     * @param {int} page: page number
-     * @param {int} index: position in the page
+     * @param {BaseItem} item: item to append to the grid
+     * @param {number} page: page number
+     * @param {number} index: position in the page
      *
      * Adds @item to the grid. @item must not be part of the grid.
      *
@@ -1344,8 +1398,8 @@ var IconGrid = GObject.registerClass({
     /**
      * moveItem:
      * @param {Clutter.Actor} item: item to move
-     * @param {int} newPage: new page of the item
-     * @param {int} newPosition: new page of the item
+     * @param {number} newPage: new page of the item
+     * @param {number} newPosition: new page of the item
      *
      * Moves @item to the grid. @item must be part of the grid.
      */
@@ -1369,7 +1423,7 @@ var IconGrid = GObject.registerClass({
 
     /**
      * goToPage:
-     * @param {int} pageIndex: page index
+     * @param {number} pageIndex: page index
      * @param {boolean} animate: animate the page transition
      *
      * Moves the current page to @pageIndex. @pageIndex must be a valid page
@@ -1405,11 +1459,11 @@ var IconGrid = GObject.registerClass({
 
     /**
      * getItemPage:
-     * @param {BaseIcon} item: the item
+     * @param {BaseIcon["prototype"]} item: the item
      *
      * Retrieves the page @item is in, or -1 if @item is not part of the grid.
      *
-     * @returns {int} the page where @item is in
+     * @returns {number} the page where @item is in
      */
     getItemPage(item) {
         return this.layout_manager.getItemPage(item);
@@ -1417,12 +1471,12 @@ var IconGrid = GObject.registerClass({
 
     /**
      * getItemPosition:
-     * @param {BaseIcon} item: the item
+     * @param {BaseIcon["prototype"]} item: the item
      *
      * Retrieves the position of @item is its page, or -1 if @item is not
      * part of the grid.
      *
-     * @returns {[int, int]} the page and position of @item
+     * @returns {[number, number]} the page and position of @item
      */
     getItemPosition(item) {
         if (!this.contains(item))
@@ -1434,8 +1488,8 @@ var IconGrid = GObject.registerClass({
 
     /**
      * getItemAt:
-     * @param {int} page: the page
-     * @param {int} position: the position in page
+     * @param {number} page: the page
+     * @param {number} position: the position in page
      *
      * Retrieves the item at @page and @position.
      *
@@ -1448,7 +1502,7 @@ var IconGrid = GObject.registerClass({
 
     /**
      * getItemsAtPage:
-     * @param {int} page: the page index
+     * @param {number} page: the page index
      *
      * Retrieves the children at page @page, including invisible children.
      *
@@ -1553,11 +1607,9 @@ var IconGrid = GObject.registerClass({
                     duration: ANIMATION_TIME_IN,
                     mode: Clutter.AnimationMode.EASE_IN_OUT_QUAD,
                     delay,
+                    ...(isLastItem ? { onComplete: this._animationDone.bind(this) } : {})
                 };
 
-                if (isLastItem)
-                    movementParams.onComplete = this._animationDone.bind(this);
-
                 fadeParams = {
                     opacity: 255,
                     duration: ANIMATION_FADE_IN_TIME_FOR_ITEM,
@@ -1579,11 +1631,9 @@ var IconGrid = GObject.registerClass({
                     duration: ANIMATION_TIME_OUT,
                     mode: Clutter.AnimationMode.EASE_IN_OUT_QUAD,
                     delay,
+                    ...(isLastItem ? { onComplete: this._animationDone.bind(this) } : {})
                 };
 
-                if (isLastItem)
-                    movementParams.onComplete = this._animationDone.bind(this);
-
                 fadeParams = {
                     opacity: 0,
                     duration: ANIMATION_FADE_IN_TIME_FOR_ITEM,
@@ -1597,6 +1647,9 @@ var IconGrid = GObject.registerClass({
         });
     }
 
+    /**
+     * @param {GridMode[]} modes 
+     */
     setGridModes(modes) {
         this._gridModes = modes ? modes : defaultGridModes;
         this.queue_relayout();
@@ -1604,7 +1657,7 @@ var IconGrid = GObject.registerClass({
 
     getDropTarget(x, y) {
         const layoutManager = this.layout_manager;
-        return layoutManager.getDropTarget(x, y, this._currentPage);
+        return layoutManager.getDropTarget(x, y);
     }
 
     get itemsPerPage() {
diff --git a/js/ui/inhibitShortcutsDialog.js b/js/ui/inhibitShortcutsDialog.js
index b20e67285e..23734da14d 100644
--- a/js/ui/inhibitShortcutsDialog.js
+++ b/js/ui/inhibitShortcutsDialog.js
@@ -1,9 +1,17 @@
 /* exported InhibitShortcutsDialog */
-const { Clutter, Gio, GLib, GObject, Gtk, Meta, Pango, Shell, St } = imports.gi;
-
-const Dialog = imports.ui.dialog;
-const ModalDialog = imports.ui.modalDialog;
-const PermissionStore = imports.misc.permissionStore;
+import Clutter from 'gi://Clutter';
+import Gio from 'gi://Gio';
+import GLib from 'gi://GLib';
+import GObject from 'gi://GObject';
+import Gtk from 'gi://Gtk';
+import Meta from 'gi://Meta';
+import Pango from 'gi://Pango';
+import Shell from 'gi://Shell';
+import St from 'gi://St';
+
+import * as Dialog from './dialog.js';
+import * as ModalDialog from './modalDialog.js';
+import * as PermissionStore from '../misc/permissionStore.js';
 
 const WAYLAND_KEYBINDINGS_SCHEMA = 'org.gnome.mutter.wayland.keybindings';
 
@@ -13,14 +21,17 @@ const APP_PERMISSIONS_ID = 'shortcuts-inhibitor';
 const GRANTED = 'GRANTED';
 const DENIED = 'DENIED';
 
-var DialogResponse = Meta.InhibitShortcutsDialogResponse;
+export const DialogResponse = Meta.InhibitShortcutsDialogResponse;
 
-var InhibitShortcutsDialog = GObject.registerClass({
+export const InhibitShortcutsDialog = GObject.registerClass({
     Implements: [Meta.InhibitShortcutsDialog],
     Properties: {
         'window': GObject.ParamSpec.override('window', Meta.InhibitShortcutsDialog),
     },
 }, class InhibitShortcutsDialog extends GObject.Object {
+    /**
+     * @param {*} window 
+     */
     _init(window) {
         super._init();
         this._window = window;
@@ -59,7 +70,7 @@ var InhibitShortcutsDialog = GObject.registerClass({
 
         let permissions = {};
         permissions[this._app.get_id()] = [grant];
-        let data = GLib.Variant.new('av', {});
+        let data = GLib.Variant.new('av', []);
 
         this._permStore.SetRemote(APP_PERMISSIONS_TABLE,
                                   true,
diff --git a/js/ui/init.js b/js/ui/init.js
index a0fe63343f..6dfe9ff303 100644
--- a/js/ui/init.js
+++ b/js/ui/init.js
@@ -2,5 +2,15 @@ import { setConsoleLogDomain } from 'console';
 
 setConsoleLogDomain('GNOME Shell');
 
-imports.ui.environment.init();
-imports.ui.main.start();
+import "./environment.js";
+
+import("./main.js")
+  .then(({ main }) => {
+    main.start();
+  })
+  .catch((error) => {
+    logError(error);
+  })
+  .finally(() => {
+    log("Main imported.");
+  });
\ No newline at end of file
diff --git a/js/ui/kbdA11yDialog.js b/js/ui/kbdA11yDialog.js
index a45e02443d..e32480e9d0 100644
--- a/js/ui/kbdA11yDialog.js
+++ b/js/ui/kbdA11yDialog.js
@@ -1,14 +1,18 @@
 /* exported KbdA11yDialog */
-const { Clutter, Gio, GObject } = imports.gi;
+import Meta from 'gi://Meta';
+import Clutter from 'gi://Clutter';
+import Gio from 'gi://Gio';
+import GObject from 'gi://GObject';
 
-const Dialog = imports.ui.dialog;
-const ModalDialog = imports.ui.modalDialog;
+
+import * as Dialog from './dialog.js';
+import * as ModalDialog from './modalDialog.js';
 
 const KEYBOARD_A11Y_SCHEMA    = 'org.gnome.desktop.a11y.keyboard';
 const KEY_STICKY_KEYS_ENABLED = 'stickykeys-enable';
 const KEY_SLOW_KEYS_ENABLED   = 'slowkeys-enable';
 
-var KbdA11yDialog = GObject.registerClass(
+export const KbdA11yDialog = GObject.registerClass(
 class KbdA11yDialog extends GObject.Object {
     _init() {
         super._init();
@@ -25,17 +29,23 @@ class KbdA11yDialog extends GObject.Object {
         let title, description;
         let key, enabled;
 
-        if (whatChanged & Clutter.KeyboardA11yFlags.SLOW_KEYS_ENABLED) {
+        const META_A11Y_SLOW_KEYS_ENABLED = 1 << 3;
+        const META_A11Y_STICKY_KEYS_ENABLED = 1 << 10;
+        // FIXME:
+        // This change 
https://github.com/GNOME/mutter/commit/c3acaeb25127a7520ecc0d3edbb3d0cc53b5634e#diff-8da73b762bc44dbbfb6a00938e37693e5e3fc3266673275502117ea932cf7675R55
+        // made Clutter.KeyboardA11yFlags turn into Meta.KeyboardA11yFlags but
+        // also removed it from introspection.
+        if (whatChanged & /* Meta.KeyboardA11yFlags */ META_A11Y_SLOW_KEYS_ENABLED) {
             key = KEY_SLOW_KEYS_ENABLED;
-            enabled = (newFlags & Clutter.KeyboardA11yFlags.SLOW_KEYS_ENABLED) > 0;
+            enabled = (newFlags & META_A11Y_SLOW_KEYS_ENABLED) > 0;
             title = enabled
                 ? _("Slow Keys Turned On")
                 : _("Slow Keys Turned Off");
             description = _('You just held down the Shift key for 8 seconds. This is the shortcut ' +
                             'for the Slow Keys feature, which affects the way your keyboard works.');
-        } else if (whatChanged & Clutter.KeyboardA11yFlags.STICKY_KEYS_ENABLED) {
+        } else if (whatChanged & META_A11Y_STICKY_KEYS_ENABLED) {
             key = KEY_STICKY_KEYS_ENABLED;
-            enabled = (newFlags & Clutter.KeyboardA11yFlags.STICKY_KEYS_ENABLED) > 0;
+            enabled = (newFlags & META_A11Y_STICKY_KEYS_ENABLED) > 0;
             title = enabled
                 ? _("Sticky Keys Turned On")
                 : _("Sticky Keys Turned Off");
diff --git a/js/ui/keyboard.js b/js/ui/keyboard.js
index 3e188a99b6..e4d43ecfa3 100644
--- a/js/ui/keyboard.js
+++ b/js/ui/keyboard.js
@@ -1,16 +1,22 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported KeyboardManager */
-
-const { Clutter, Gio, GLib, GObject, Graphene, Meta, Shell, St } = imports.gi;
-const Signals = imports.misc.signals;
-
-const EdgeDragAction = imports.ui.edgeDragAction;
-const InputSourceManager = imports.ui.status.keyboard;
-const IBusManager = imports.misc.ibusManager;
-const BoxPointer = imports.ui.boxpointer;
-const Main = imports.ui.main;
-const PageIndicators = imports.ui.pageIndicators;
-const PopupMenu = imports.ui.popupMenu;
+import Clutter from 'gi://Clutter';
+import St from 'gi://St';
+import Gio from 'gi://Gio';
+import GLib from 'gi://GLib';
+import GObject from 'gi://GObject';
+import Shell from 'gi://Shell';
+import Meta from 'gi://Meta';
+import Graphene from 'gi://Graphene';
+import * as Signals from '../misc/signals.js';
+
+import * as EdgeDragAction from './edgeDragAction.js';
+import * as InputSourceManager from './status/keyboard.js';
+import * as IBusManager from '../misc/ibusManager.js';
+import * as BoxPointer from './boxpointer.js';
+import Main from './main.js';
+import * as PageIndicators from './pageIndicators.js';
+import * as PopupMenu from './popupMenu.js';
 
 var KEYBOARD_ANIMATION_TIME = 150;
 var KEYBOARD_REST_TIME = KEYBOARD_ANIMATION_TIME * 2;
@@ -50,7 +56,7 @@ const defaultKeysPost = [
      [{ action: 'emoji', icon: 'face-smile-symbolic' }, { action: 'languageMenu', extraClassName: 
'layout-key', icon: 'keyboard-layout-filled-symbolic' }, { action: 'hide', extraClassName: 'hide-key', icon: 
'go-down-symbolic' }]],
 ];
 
-var AspectContainer = GObject.registerClass(
+export const AspectContainer = GObject.registerClass(
 class AspectContainer extends St.Widget {
     _init(params) {
         super._init(params);
@@ -62,6 +68,9 @@ class AspectContainer extends St.Widget {
         this.queue_relayout();
     }
 
+    /**
+     * @returns {[number, number]}
+     */
     vfunc_get_preferred_width(forHeight) {
         let [min, nat] = super.vfunc_get_preferred_width(forHeight);
 
@@ -71,6 +80,9 @@ class AspectContainer extends St.Widget {
         return [min, nat];
     }
 
+    /**
+     * @returns {[number, number]}
+     */
     vfunc_get_preferred_height(forWidth) {
         let [min, nat] = super.vfunc_get_preferred_height(forWidth);
 
@@ -102,7 +114,7 @@ class AspectContainer extends St.Widget {
     }
 });
 
-var KeyContainer = GObject.registerClass(
+export const KeyContainer = GObject.registerClass(
 class KeyContainer extends St.Widget {
     _init() {
         let gridLayout = new Clutter.GridLayout({ orientation: Clutter.Orientation.HORIZONTAL,
@@ -120,6 +132,8 @@ class KeyContainer extends St.Widget {
 
         this._currentRow = null;
         this._rows = [];
+
+        this.shiftKeys = [];
     }
 
     appendRow() {
@@ -183,7 +197,7 @@ class KeyContainer extends St.Widget {
     }
 });
 
-var Suggestions = GObject.registerClass(
+export const Suggestions = GObject.registerClass(
 class Suggestions extends St.BoxLayout {
     _init() {
         super._init({
@@ -205,7 +219,7 @@ class Suggestions extends St.BoxLayout {
     }
 });
 
-var LanguageSelectionPopup = class extends PopupMenu.PopupMenu {
+export class LanguageSelectionPopup extends PopupMenu.PopupMenu {
     constructor(actor) {
         super(actor, 0.5, St.Side.BOTTOM);
 
@@ -268,7 +282,7 @@ var LanguageSelectionPopup = class extends PopupMenu.PopupMenu {
     }
 };
 
-var Key = GObject.registerClass({
+export const Key = GObject.registerClass({
     Signals: {
         'activated': {},
         'long-press': {},
@@ -288,6 +302,7 @@ var Key = GObject.registerClass({
         this.add_child(this.keyButton);
         this.connect('destroy', this._onDestroy.bind(this));
 
+        this._keyvalPress = false;
         this._extendedKeys = extendedKeys;
         this._extendedKeyboard = null;
         this._pressTimeoutId = 0;
@@ -516,7 +531,7 @@ var Key = GObject.registerClass({
     }
 });
 
-var KeyboardModel = class {
+export class KeyboardModel {
     constructor(groupName) {
         let names = [groupName];
         if (groupName.includes('+'))
@@ -549,7 +564,7 @@ var KeyboardModel = class {
     }
 };
 
-var FocusTracker = class extends Signals.EventEmitter {
+export class FocusTracker extends Signals.EventEmitter {
     constructor() {
         super();
 
@@ -664,7 +679,7 @@ var FocusTracker = class extends Signals.EventEmitter {
     }
 };
 
-var EmojiPager = GObject.registerClass({
+export const EmojiPager = GObject.registerClass({
     Properties: {
         'delta': GObject.ParamSpec.int(
             'delta', 'delta', 'delta',
@@ -952,7 +967,7 @@ var EmojiPager = GObject.registerClass({
     }
 });
 
-var EmojiSelection = GObject.registerClass({
+export const EmojiSelection = GObject.registerClass({
     Signals: {
         'emoji-selected': { param_types: [GObject.TYPE_STRING] },
         'close-request': {},
@@ -1110,7 +1125,7 @@ var EmojiSelection = GObject.registerClass({
     }
 });
 
-var Keypad = GObject.registerClass({
+export const Keypad = GObject.registerClass({
     Signals: {
         'keyval': { param_types: [GObject.TYPE_UINT] },
     },
@@ -1162,7 +1177,7 @@ var Keypad = GObject.registerClass({
     }
 });
 
-var KeyboardManager = class KeyBoardManager {
+export class KeyboardManager {
     constructor() {
         this._keyboard = null;
         this._a11yApplicationsSettings = new Gio.Settings({ schema_id: A11Y_APPLICATIONS_SCHEMA });
@@ -1269,7 +1284,7 @@ var KeyboardManager = class KeyBoardManager {
     }
 };
 
-var Keyboard = GObject.registerClass({
+export const Keyboard = GObject.registerClass({
     Signals: {
         'visibility-changed': {},
     },
@@ -1301,7 +1316,7 @@ var Keyboard = GObject.registerClass({
         if (!Meta.is_wayland_compositor()) {
             this._connectSignal(this._focusTracker, 'focus-changed', (_tracker, focused) => {
                 if (focused)
-                    this.open(Main.layoutManager.focusIndex);
+                    this.open(!!Main.layoutManager.focusIndex);
                 else
                     this.close();
             });
@@ -1441,7 +1456,7 @@ var Keyboard = GObject.registerClass({
         // Showing an extended key popup and clicking a key from the extended keys
         // will grab focus, but ignore that
         let extendedKeysWereFocused = this._focusInExtendedKeys;
-        this._focusInExtendedKeys = focus && (focus._extendedKeys || focus.extendedKey);
+        this._focusInExtendedKeys = focus && (focus instanceof St.Button && (focus._extendedKeys || 
focus.extendedKey));
         if (this._focusInExtendedKeys || extendedKeysWereFocused)
             return;
 
@@ -1452,7 +1467,7 @@ var Keyboard = GObject.registerClass({
 
         if (!this._showIdleId) {
             this._showIdleId = GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE, () => {
-                this.open(Main.layoutManager.focusIndex);
+                this.open(!!Main.layoutManager.focusIndex);
                 this._showIdleId = 0;
                 return GLib.SOURCE_REMOVE;
             });
@@ -1473,7 +1488,6 @@ var Keyboard = GObject.registerClass({
             let level = i >= 1 && levels.length == 3 ? i + 1 : i;
 
             let layout = new KeyContainer();
-            layout.shiftKeys = [];
 
             this._loadRows(currentLevel, level, levels.length, layout);
             layers[level] = layout;
@@ -1655,6 +1669,12 @@ var Keyboard = GObject.registerClass({
             this._loadDefaultKeys(post, layout, numLevels, row.length);
     }
 
+    /**
+     * @param {*} model 
+     * @param {number} level 
+     * @param {number} numLevels 
+     * @param {KeyContainer["prototype"]} layout 
+     */
     _loadRows(model, level, numLevels, layout) {
         let rows = model.rows;
         for (let i = 0; i < rows.length; ++i) {
@@ -1708,8 +1728,7 @@ var Keyboard = GObject.registerClass({
     }
 
     _onKeyboardGroupsChanged() {
-        let nonGroupActors = [this._emojiSelection, this._keypad];
-        this._aspectContainer.get_children().filter(c => !nonGroupActors.includes(c)).forEach(c => {
+        this._aspectContainer.get_children().filter(c => c !== this._emojiSelection && c !== 
this._keypad).forEach(c => {
             c.destroy();
         });
 
@@ -1747,7 +1766,7 @@ var Keyboard = GObject.registerClass({
             return;
 
         if (enabled)
-            this.open(Main.layoutManager.focusIndex);
+            this.open(!!Main.layoutManager.focusIndex);
         else
             this.close();
     }
@@ -2037,7 +2056,7 @@ var Keyboard = GObject.registerClass({
     }
 });
 
-var KeyboardController = class extends Signals.EventEmitter {
+export class KeyboardController extends Signals.EventEmitter {
     constructor() {
         super();
 
diff --git a/js/ui/layout.js b/js/ui/layout.js
index 39d20e027b..c4e25cc3e8 100644
--- a/js/ui/layout.js
+++ b/js/ui/layout.js
@@ -1,21 +1,27 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported MonitorConstraint, LayoutManager */
 
-const { Clutter, Gio, GLib, GObject, Meta, Shell, St } = imports.gi;
-const Signals = imports.misc.signals;
+import Clutter from 'gi://Clutter';
+import Gio from 'gi://Gio';
+import GLib from 'gi://GLib';
+import GObject from 'gi://GObject';
+import Meta from 'gi://Meta';
+import Shell from 'gi://Shell';
+import St from 'gi://St';
+import * as Signals from '../misc/signals.js';
 
-const Background = imports.ui.background;
-const BackgroundMenu = imports.ui.backgroundMenu;
+import * as Background from './background.js';
+import * as BackgroundMenu from './backgroundMenu.js';
 
-const DND = imports.ui.dnd;
-const Main = imports.ui.main;
-const Ripples = imports.ui.ripples;
+import * as DND from './dnd.js';
+import Main from './main.js';
+import * as Ripples from './ripples.js';
 
-var STARTUP_ANIMATION_TIME = 500;
-var BACKGROUND_FADE_ANIMATION_TIME = 1000;
+export let STARTUP_ANIMATION_TIME = 500;
+export let BACKGROUND_FADE_ANIMATION_TIME = 1000;
 
-var HOT_CORNER_PRESSURE_THRESHOLD = 100; // pixels
-var HOT_CORNER_PRESSURE_TIMEOUT = 1000; // ms
+export let HOT_CORNER_PRESSURE_THRESHOLD = 100; // pixels
+export let HOT_CORNER_PRESSURE_TIMEOUT = 1000; // ms
 
 function isPopupMetaWindow(actor) {
     switch (actor.meta_window.get_window_type()) {
@@ -28,7 +34,7 @@ function isPopupMetaWindow(actor) {
     }
 }
 
-var MonitorConstraint = GObject.registerClass({
+export const MonitorConstraint = GObject.registerClass({
     Properties: {
         'primary': GObject.ParamSpec.boolean('primary',
                                              'Primary', 'Track primary monitor',
@@ -44,6 +50,9 @@ var MonitorConstraint = GObject.registerClass({
                                                false),
     },
 }, class MonitorConstraint extends Clutter.Constraint {
+    /**
+     * @param {*} props 
+     */
     _init(props) {
         this._primary = false;
         this._index = -1;
@@ -145,7 +154,7 @@ var MonitorConstraint = GObject.registerClass({
     }
 });
 
-var Monitor = class Monitor {
+export class Monitor {
     constructor(index, geometry, geometryScale) {
         this.index = index;
         this.x = geometry.x;
@@ -162,11 +171,17 @@ var Monitor = class Monitor {
 
 const UiActor = GObject.registerClass(
 class UiActor extends St.Widget {
+    /**
+     * @returns {[number, number]}
+     */
     vfunc_get_preferred_width(_forHeight) {
         let width = global.stage.width;
         return [width, width];
     }
 
+    /**
+     * @returns {[number, number]}
+     */
     vfunc_get_preferred_height(_forWidth) {
         let height = global.stage.height;
         return [height, height];
@@ -179,7 +194,7 @@ const defaultParams = {
     affectsInputRegion: true,
 };
 
-var LayoutManager = GObject.registerClass({
+export const LayoutManager = GObject.registerClass({
     Signals: {
         'hot-corners-changed': {},
         'startup-complete': {},
@@ -244,7 +259,7 @@ var LayoutManager = GObject.registerClass({
                                         trackFullscreen: true });
         this.panelBox.connect('notify::allocation',
                               this._panelBoxChanged.bind(this));
-
+        log('initing?');
         this.modalDialogGroup = new St.Widget({ name: 'modalDialogGroup',
                                                 layout_manager: new Clutter.BinLayout() });
         this.uiGroup.add_actor(this.modalDialogGroup);
@@ -296,7 +311,7 @@ var LayoutManager = GObject.registerClass({
     // This is called by Main after everything else is constructed
     init() {
         Main.sessionMode.connect('updated', this._sessionUpdated.bind(this));
-
+        log('testing?');
         this._loadBackground();
     }
 
@@ -503,6 +518,7 @@ var LayoutManager = GObject.registerClass({
     }
 
     _panelBoxChanged() {
+        log('panel box changed')
         this._updatePanelBarrier();
 
         let size = this.panelBox.height;
@@ -532,6 +548,7 @@ var LayoutManager = GObject.registerClass({
     }
 
     _monitorsChanged() {
+        log('monitor?');
         this._updateMonitors();
         this._updateBoxes();
         this._updateHotCorners();
@@ -606,8 +623,9 @@ var LayoutManager = GObject.registerClass({
         this._systemBackground.add_constraint(constraint);
 
         let signalId = this._systemBackground.connect('loaded', () => {
+            log('loaded!');
             this._systemBackground.disconnect(signalId);
-
+log('p2');
             // We're mostly prepared for the startup animation
             // now, but since a lot is going on asynchronously
             // during startup, let's defer the startup animation
@@ -615,6 +633,7 @@ var LayoutManager = GObject.registerClass({
             // This helps to prevent us from running the animation
             // when the system is bogged down
             const id = GLib.idle_add(GLib.PRIORITY_LOW, () => {
+                log('idling...');
                 this._systemBackground.show();
                 global.stage.show();
                 this._prepareStartupAnimation();
@@ -640,6 +659,7 @@ var LayoutManager = GObject.registerClass({
     // of the screen.
 
     async _prepareStartupAnimation() {
+        log('preparing...');
         // During the initial transition, add a simple actor to block all events,
         // so they don't get delivered to X11 windows that have been transformed.
         this._coverPane = new Clutter.Actor({ opacity: 0,
@@ -676,25 +696,31 @@ var LayoutManager = GObject.registerClass({
             }
 
             global.window_group.set_clip(monitor.x, monitor.y, monitor.width, monitor.height);
-
+log('update backgrounds?');
             await this._updateBackgrounds();
         }
 
+        log('prepared?');
+
         this.emit('startup-prepared');
 
         this._startupAnimation();
     }
 
     _startupAnimation() {
-        if (Meta.is_restart())
+        if (Meta.is_restart()) {
+            log('testing')
             this._startupAnimationComplete();
-        else if (Main.sessionMode.isGreeter)
+        } else if (Main.sessionMode.isGreeter)
             this._startupAnimationGreeter();
-        else
+        else {
+            log ('test 213');
             this._startupAnimationSession();
+        }
     }
 
     _startupAnimationGreeter() {
+        log('greet start');
         this.panelBox.ease({
             translation_y: 0,
             duration: STARTUP_ANIMATION_TIME,
@@ -704,6 +730,7 @@ var LayoutManager = GObject.registerClass({
     }
 
     _startupAnimationSession() {
+        log('greet sesh start');
         const onComplete = () => this._startupAnimationComplete();
         if (Main.sessionMode.hasOverview) {
             Main.overview.runStartupAnimation(onComplete);
@@ -720,6 +747,7 @@ var LayoutManager = GObject.registerClass({
     }
 
     _startupAnimationComplete() {
+        log('Animation compl')
         this._coverPane.destroy();
         this._coverPane = null;
 
@@ -852,14 +880,15 @@ var LayoutManager = GObject.registerClass({
         if (this._findActor(actor) != -1)
             throw new Error('trying to re-track existing chrome actor');
 
-        let actorData = { ...defaultParams, ...params };
-        actorData.actor = actor;
-        actorData.visibleId = actor.connect('notify::visible',
-                                            this._queueUpdateRegions.bind(this));
-        actorData.allocationId = actor.connect('notify::allocation',
-                                               this._queueUpdateRegions.bind(this));
-        actorData.destroyId = actor.connect('destroy',
-                                            this._untrackActor.bind(this));
+        let actorData = { ...defaultParams, ...params,
+      actor: actor,
+        isibleId : actor.connect('notify::visible',
+                                            this._queueUpdateRegions.bind(this)),
+      allocationId : actor.connect('notify::allocation',
+                                               this._queueUpdateRegions.bind(this)),
+      destroyId: actor.connect('destroy',
+                                            this._untrackActor.bind(this)),
+        };
         // Note that destroying actor will unset its parent, so we don't
         // need to connect to 'destroy' too.
 
@@ -1069,7 +1098,7 @@ var LayoutManager = GObject.registerClass({
 //
 // This class manages a "hot corner" that can toggle switching to
 // overview.
-var HotCorner = GObject.registerClass(
+export const HotCorner = GObject.registerClass(
 class HotCorner extends Clutter.Actor {
     _init(layoutManager, monitor, x, y) {
         super._init();
@@ -1227,7 +1256,7 @@ class HotCorner extends Clutter.Actor {
     }
 });
 
-var PressureBarrier = class PressureBarrier extends Signals.EventEmitter {
+export class PressureBarrier extends Signals.EventEmitter {
     constructor(threshold, timeout, actionMode) {
         super();
 
diff --git a/js/ui/lightbox.js b/js/ui/lightbox.js
index 9e66ffd58f..3fb71ab404 100644
--- a/js/ui/lightbox.js
+++ b/js/ui/lightbox.js
@@ -1,12 +1,15 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported Lightbox */
 
-const { Clutter, GObject, Shell, St } = imports.gi;
+import Clutter from 'gi://Clutter';
+import GObject from 'gi://GObject';
+import Shell from 'gi://Shell';
+import St from 'gi://St';
 
 
-var DEFAULT_FADE_FACTOR = 0.4;
-var VIGNETTE_BRIGHTNESS = 0.5;
-var VIGNETTE_SHARPNESS = 0.7;
+export let DEFAULT_FADE_FACTOR = 0.4;
+export let VIGNETTE_BRIGHTNESS = 0.5;
+export let VIGNETTE_SHARPNESS = 0.7;
 
 const VIGNETTE_DECLARATIONS = '\
 uniform float brightness;\n\
@@ -21,7 +24,7 @@ t = clamp(t, 0.0, 1.0);\n\
 float pixel_brightness = mix(1.0, 1.0 - vignette_sharpness, t);\n\
 cogl_color_out.a = cogl_color_out.a * (1 - pixel_brightness * brightness);';
 
-var RadialShaderEffect = GObject.registerClass({
+export const RadialShaderEffect = GObject.registerClass({
     Properties: {
         'brightness': GObject.ParamSpec.float(
             'brightness', 'brightness', 'brightness',
@@ -102,7 +105,7 @@ var RadialShaderEffect = GObject.registerClass({
  * @container and will track any changes in its size. You can override
  * this by passing an explicit width and height in @params.
  */
-var Lightbox = GObject.registerClass({
+export const Lightbox = GObject.registerClass({
     Properties: {
         'active': GObject.ParamSpec.boolean(
             'active', 'active', 'active', GObject.ParamFlags.READABLE, false),
diff --git a/js/ui/locatePointer.js b/js/ui/locatePointer.js
index 6ae29419a3..41ccf34688 100644
--- a/js/ui/locatePointer.js
+++ b/js/ui/locatePointer.js
@@ -1,14 +1,14 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported LocatePointer */
 
-const { Gio } = imports.gi;
-const Ripples = imports.ui.ripples;
-const Main = imports.ui.main;
+import Gio from 'gi://Gio';
+import * as Ripples from './ripples.js';
+import Main from './main.js';
 
 const LOCATE_POINTER_KEY = "locate-pointer";
 const LOCATE_POINTER_SCHEMA = "org.gnome.desktop.interface";
 
-var LocatePointer = class {
+export class LocatePointer {
     constructor() {
         this._settings = new Gio.Settings({ schema_id: LOCATE_POINTER_SCHEMA });
         this._settings.connect(`changed::${LOCATE_POINTER_KEY}`, () => this._syncEnabled());
diff --git a/js/ui/lookingGlass.js b/js/ui/lookingGlass.js
index 3a39b7de97..d3798613b3 100644
--- a/js/ui/lookingGlass.js
+++ b/js/ui/lookingGlass.js
@@ -1,16 +1,24 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported LookingGlass */
 
-const { Clutter, Cogl, Gio, GLib, GObject,
-        Graphene, Meta, Pango, Shell, St } = imports.gi;
-const Signals = imports.misc.signals;
-const System = imports.system;
-
-const History = imports.misc.history;
-const ExtensionUtils = imports.misc.extensionUtils;
-const ShellEntry = imports.ui.shellEntry;
-const Main = imports.ui.main;
-const JsParse = imports.misc.jsParse;
+import Clutter from 'gi://Clutter';
+import Cogl from 'gi://Cogl';
+import Gio from 'gi://Gio';
+import GLib from 'gi://GLib';
+import GObject from 'gi://GObject';
+import Graphene from 'gi://Graphene';
+import Meta from 'gi://Meta';
+import Pango from 'gi://Pango';
+import Shell from 'gi://Shell';
+import St from 'gi://St';
+import * as Signals from '../misc/signals.js';
+import System from 'system';
+
+import * as History from '../misc/history.js';
+import * as ExtensionUtils from '../misc/extensionUtils.js';
+import * as ShellEntry from './shellEntry.js';
+import Main from './main.js';
+import * as JsParse from '../misc/jsParse.js';
 
 const { ExtensionState } = ExtensionUtils;
 
@@ -18,7 +26,7 @@ const CHEVRON = '>>> ';
 
 /* Imports...feel free to add here as needed */
 var commandHeader = 'const { Clutter, Gio, GLib, GObject, Meta, Shell, St } = imports.gi; ' +
-                    'const Main = imports.ui.main; ' +
+                    'const Main = getMain(); ' +
                     /* Utility functions...we should probably be able to use these
                      * in the shell core code too. */
                     'const stage = global.stage; ' +
@@ -29,9 +37,10 @@ var commandHeader = 'const { Clutter, Gio, GLib, GObject, Meta, Shell, St } = im
 
 const HISTORY_KEY = 'looking-glass-history';
 // Time between tabs for them to count as a double-tab event
-var AUTO_COMPLETE_DOUBLE_TAB_DELAY = 500;
-var AUTO_COMPLETE_SHOW_COMPLETION_ANIMATION_DURATION = 200;
-var AUTO_COMPLETE_GLOBAL_KEYWORDS = _getAutoCompleteGlobalKeywords();
+
+export let AUTO_COMPLETE_DOUBLE_TAB_DELAY = 500;
+export let AUTO_COMPLETE_SHOW_COMPLETION_ANIMATION_DURATION = 200;
+export let AUTO_COMPLETE_GLOBAL_KEYWORDS = _getAutoCompleteGlobalKeywords();
 
 const LG_ANIMATION_TIME = 500;
 
@@ -45,7 +54,7 @@ function _getAutoCompleteGlobalKeywords() {
     return keywords.concat(windowProperties).concat(headerProperties);
 }
 
-var AutoComplete = class AutoComplete extends Signals.EventEmitter {
+export class AutoComplete extends Signals.EventEmitter {
     constructor(entry) {
         super();
 
@@ -109,8 +118,7 @@ var AutoComplete = class AutoComplete extends Signals.EventEmitter {
     }
 };
 
-
-var Notebook = GObject.registerClass({
+export const Notebook = GObject.registerClass({
     Signals: { 'selection': { param_types: [Clutter.Actor.$gtype] } },
 }, class Notebook extends St.BoxLayout {
     _init() {
@@ -253,8 +261,14 @@ function objectToString(o) {
     }
 }
 
-var ObjLink = GObject.registerClass(
+export const ObjLink = GObject.registerClass(
+/** @extends {St.Button<Clutter.Text>} */
 class ObjLink extends St.Button {
+    /**
+     * @param {*} lookingGlass 
+     * @param {*} o 
+     * @param {*} title 
+     */
     _init(lookingGlass, o, title) {
         let text;
         if (title)
@@ -281,8 +295,14 @@ class ObjLink extends St.Button {
     }
 });
 
-var Result = GObject.registerClass(
+export const Result = GObject.registerClass(
 class Result extends St.BoxLayout {
+    /**
+     * @param {*} lookingGlass 
+     * @param {*} command 
+     * @param {*} o 
+     * @param {*} index 
+     */
     _init(lookingGlass, command, o, index) {
         super._init({ vertical: true });
 
@@ -304,8 +324,11 @@ class Result extends St.BoxLayout {
     }
 });
 
-var WindowList = GObject.registerClass({
+export const WindowList = GObject.registerClass({
 }, class WindowList extends St.BoxLayout {
+    /**
+     * @param {*} lookingGlass 
+     */
     _init(lookingGlass) {
         super._init({ name: 'Windows', vertical: true, style: 'spacing: 8px' });
         let tracker = Shell.WindowTracker.get_default();
@@ -357,8 +380,11 @@ var WindowList = GObject.registerClass({
     }
 });
 
-var ObjInspector = GObject.registerClass(
+export const ObjInspector = GObject.registerClass(
 class ObjInspector extends St.ScrollView {
+    /**
+     * @param {*} lookingGlass 
+     */
     _init(lookingGlass) {
         super._init({
             pivot_point: new Graphene.Point({ x: 0.5, y: 0.5 }),
@@ -397,7 +423,8 @@ class ObjInspector extends St.ScrollView {
             text: 'Inspecting: %s: %s'.format(typeof obj, objectToString(obj)),
             x_expand: true,
         });
-        label.single_line_mode = true;
+        // TODO: Is this the intended code?
+        label.clutterText.single_line_mode = true;
         hbox.add_child(label);
         let button = new St.Button({ label: 'Insert', style_class: 'lg-obj-inspector-button' });
         button.connect('clicked', this._onInsert.bind(this));
@@ -475,7 +502,7 @@ class ObjInspector extends St.ScrollView {
     }
 });
 
-var RedBorderEffect = GObject.registerClass(
+export const RedBorderEffect = GObject.registerClass(
 class RedBorderEffect extends Clutter.Effect {
     _init() {
         super._init();
@@ -527,7 +554,7 @@ class RedBorderEffect extends Clutter.Effect {
     }
 });
 
-var Inspector = GObject.registerClass({
+export const Inspector = GObject.registerClass({
     Signals: { 'closed': {},
                'target': { param_types: [Clutter.Actor.$gtype, GObject.TYPE_DOUBLE, GObject.TYPE_DOUBLE] } },
 }, class Inspector extends Clutter.Actor {
@@ -667,7 +694,7 @@ var Inspector = GObject.registerClass({
     }
 });
 
-var Extensions = GObject.registerClass({
+export const Extensions = GObject.registerClass({
 }, class Extensions extends St.BoxLayout {
     _init(lookingGlass) {
         super._init({ vertical: true, name: 'lookingGlassExtensions' });
@@ -820,7 +847,7 @@ var Extensions = GObject.registerClass({
 });
 
 
-var ActorLink = GObject.registerClass({
+export const ActorLink = GObject.registerClass({
     Signals: {
         'inspect-actor': {},
     },
@@ -877,7 +904,7 @@ var ActorLink = GObject.registerClass({
     }
 });
 
-var ActorTreeViewer = GObject.registerClass(
+export const ActorTreeViewer = GObject.registerClass(
 class ActorTreeViewer extends St.BoxLayout {
     _init(lookingGlass) {
         super._init();
@@ -1004,7 +1031,7 @@ class ActorTreeViewer extends St.BoxLayout {
     }
 });
 
-var LookingGlass = GObject.registerClass(
+export const LookingGlass = GObject.registerClass(
 class LookingGlass extends St.BoxLayout {
     _init() {
         super._init({
@@ -1093,6 +1120,7 @@ class LookingGlass extends St.BoxLayout {
         this._evalBox = new St.BoxLayout({ name: 'EvalBox', vertical: true });
         notebook.appendPage('Evaluator', this._evalBox);
 
+        /** @type {St.BoxLayout<Result["prototype"]>} */
         this._resultsArea = new St.BoxLayout({
             name: 'ResultsArea',
             vertical: true,
@@ -1270,7 +1298,7 @@ class LookingGlass extends St.BoxLayout {
 
     getResult(idx) {
         try {
-            return this._resultsArea.get_child_at_index(idx - this._offset).o;
+            return /** @type {Result["prototype"]} */ (this._resultsArea.get_child_at_index(idx - 
this._offset)).o;
         } catch (e) {
             throw new Error('Unknown result at index %d'.format(idx));
         }
diff --git a/js/ui/magnifier.js b/js/ui/magnifier.js
index 8727dbd67d..ff8876a32c 100644
--- a/js/ui/magnifier.js
+++ b/js/ui/magnifier.js
@@ -1,19 +1,28 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported Magnifier */
 
-const { Atspi, Clutter, GDesktopEnums,
-        Gio, GLib, GObject, Meta, Shell, St } = imports.gi;
-const Signals = imports.misc.signals;
+import Atspi from 'gi://Atspi';
+import Clutter from 'gi://Clutter';
+import GDesktopEnums from 'gi://GDesktopEnums';
+import Gio from 'gi://Gio';
+import GLib from 'gi://GLib';
+import GObject from 'gi://GObject';
+import Meta from 'gi://Meta';
+import Shell from 'gi://Shell';
+import St from 'gi://St';
 
-const Background = imports.ui.background;
-const FocusCaretTracker = imports.ui.focusCaretTracker;
-const Main = imports.ui.main;
-const PointerWatcher = imports.ui.pointerWatcher;
+import * as Signals from '../misc/signals.js';
 
-var CROSSHAIRS_CLIP_SIZE = [100, 100];
-var NO_CHANGE = 0.0;
+import * as Background from './background.js';
+import * as FocusCaretTracker from './focusCaretTracker.js';
+import Main from './main.js';
+import * as PointerWatcher from './pointerWatcher.js';
 
-var POINTER_REST_TIME = 1000; // milliseconds
+/** @type {[number, number]} */
+export const CROSSHAIRS_CLIP_SIZE = [100, 100];
+export const NO_CHANGE = 0.0;
+
+export const POINTER_REST_TIME = 1000; // milliseconds
 
 // Settings
 const MAGNIFIER_SCHEMA          = 'org.gnome.desktop.a11y.magnifier';
@@ -39,7 +48,7 @@ const CROSS_HAIRS_OPACITY_KEY   = 'cross-hairs-opacity';
 const CROSS_HAIRS_LENGTH_KEY    = 'cross-hairs-length';
 const CROSS_HAIRS_CLIP_KEY      = 'cross-hairs-clip';
 
-var MouseSpriteContent = GObject.registerClass({
+export const MouseSpriteContent = GObject.registerClass({
     Implements: [Clutter.Content],
 }, class MouseSpriteContent extends GObject.Object {
     _init() {
@@ -72,6 +81,9 @@ var MouseSpriteContent = GObject.registerClass({
         return this._texture;
     }
 
+    /**
+     * @this {this & Clutter.Content}
+     */
     set texture(coglTexture) {
         if (this._texture == coglTexture)
             return;
@@ -87,7 +99,7 @@ var MouseSpriteContent = GObject.registerClass({
     }
 });
 
-var Magnifier = class Magnifier extends Signals.EventEmitter {
+export class Magnifier extends Signals.EventEmitter {
     constructor() {
         super();
 
@@ -98,8 +110,10 @@ var Magnifier = class Magnifier extends Signals.EventEmitter {
         let cursorTracker = Meta.CursorTracker.get_for_display(global.display);
         this._cursorTracker = cursorTracker;
 
-        this._mouseSprite = new Clutter.Actor({ request_mode: Clutter.RequestMode.CONTENT_SIZE });
-        this._mouseSprite.content = new MouseSpriteContent();
+        this._mouseSprite = new Clutter.Actor({
+            request_mode: Clutter.RequestMode.CONTENT_SIZE,
+            content: new MouseSpriteContent()
+        });
 
         // Create the first ZoomRegion and initialize it according to the
         // magnification settings.
@@ -145,7 +159,7 @@ var Magnifier = class Magnifier extends Signals.EventEmitter {
     /**
      * setActive:
      * Show/hide all the zoom regions.
-     * @param {bool} activate: Boolean to activate or de-activate the magnifier.
+     * @param {boolean} activate: Boolean to activate or de-activate the magnifier.
      */
     setActive(activate) {
         let isActive = this.isActive();
@@ -185,7 +199,7 @@ var Magnifier = class Magnifier extends Signals.EventEmitter {
 
     /**
      * isActive:
-     * @returns {bool} Whether the magnifier is active.
+     * @returns {boolean} Whether the magnifier is active.
      */
     isActive() {
         // Sufficient to check one ZoomRegion since Magnifier's active
@@ -220,7 +234,7 @@ var Magnifier = class Magnifier extends Signals.EventEmitter {
 
     /**
      * isTrackingMouse:
-     * @returns {bool} whether the magnifier is currently tracking the mouse
+     * @returns {boolean} whether the magnifier is currently tracking the mouse
      */
     isTrackingMouse() {
         return !!this._pointerWatch;
@@ -297,7 +311,7 @@ var Magnifier = class Magnifier extends Signals.EventEmitter {
     /**
      * getZoomRegions:
      * Return a list of ZoomRegion's for this Magnifier.
-     * @returns {number[]} The Magnifier's zoom region list.
+     * @returns {ZoomRegion[]} The Magnifier's zoom region list.
      */
     getZoomRegions() {
         return this._zoomRegions;
@@ -345,7 +359,7 @@ var Magnifier = class Magnifier extends Signals.EventEmitter {
     /**
      * setCrosshairsVisible:
      * Show or hide the cross hair.
-     * @param {bool} visible: Flag that indicates show (true) or hide (false).
+     * @param {boolean} visible: Flag that indicates show (true) or hide (false).
      */
     setCrosshairsVisible(visible) {
         if (visible) {
@@ -405,17 +419,6 @@ var Magnifier = class Magnifier extends Signals.EventEmitter {
             this._crossHairs.setOpacity(opacity * 255);
     }
 
-    /**
-     * getCrosshairsOpacity:
-     * @returns {number} Value between 0.0 (transparent) and 1.0 (fully opaque).
-     */
-    getCrosshairsOpacity() {
-        if (this._crossHairs)
-            return this._crossHairs.getOpacity() / 255.0;
-        else
-            return 0.0;
-    }
-
     /**
      * setCrosshairsLength:
      * Set the crosshairs length for all ZoomRegions.
@@ -445,7 +448,7 @@ var Magnifier = class Magnifier extends Signals.EventEmitter {
     /**
      * setCrosshairsClip:
      * Set whether the crosshairs are clipped at their intersection.
-     * @param {bool} clip: Flag to indicate whether to clip the crosshairs.
+     * @param {boolean} clip: Flag to indicate whether to clip the crosshairs.
      */
     setCrosshairsClip(clip) {
         if (!this._crossHairs)
@@ -458,7 +461,7 @@ var Magnifier = class Magnifier extends Signals.EventEmitter {
     /**
      * getCrosshairsClip:
      * Get whether the crosshairs are clipped by the mouse image.
-     * @returns {bool} Whether the crosshairs are clipped.
+     * @returns {boolean} Whether the crosshairs are clipped.
      */
     getCrosshairsClip() {
         if (this._crossHairs) {
@@ -577,9 +580,9 @@ var Magnifier = class Magnifier extends Signals.EventEmitter {
             if (aPref)
                 zoomRegion.setCaretTrackingMode(aPref);
 
-            aPref = this._settings.get_boolean(INVERT_LIGHTNESS_KEY);
-            if (aPref)
-                zoomRegion.setInvertLightness(aPref);
+            let aBoolPref = this._settings.get_boolean(INVERT_LIGHTNESS_KEY);
+            if (aBoolPref)
+                zoomRegion.setInvertLightness(aBoolPref);
 
             aPref = this._settings.get_double(COLOR_SATURATION_KEY);
             if (aPref)
@@ -698,7 +701,7 @@ var Magnifier = class Magnifier extends Signals.EventEmitter {
     }
 };
 
-var ZoomRegion = class ZoomRegion {
+export class ZoomRegion {
     constructor(magnifier, mouseSourceActor) {
         this._magnifier = magnifier;
         this._focusCaretTracker = new FocusCaretTracker.FocusCaretTracker();
@@ -749,11 +752,11 @@ var ZoomRegion = class ZoomRegion {
                                             this._monitorsChanged.bind(this));
         this._signalConnections.push([Main.layoutManager, id]);
 
-        id = this._focusCaretTracker.connect('caret-moved', this._updateCaret.bind(this));
-        this._signalConnections.push([this._focusCaretTracker, id]);
+        let focusId = this._focusCaretTracker.connect('caret-moved', this._updateCaret.bind(this));
+        this._signalConnections.push([this._focusCaretTracker, focusId]);
 
-        id = this._focusCaretTracker.connect('focus-changed', this._updateFocus.bind(this));
-        this._signalConnections.push([this._focusCaretTracker, id]);
+        focusId = this._focusCaretTracker.connect('focus-changed', this._updateFocus.bind(this));
+        this._signalConnections.push([this._focusCaretTracker, focusId]);
     }
 
     _disconnectSignals() {
@@ -827,7 +830,7 @@ var ZoomRegion = class ZoomRegion {
 
     /**
      * setActive:
-     * @param {bool} activate: Boolean to show/hide the ZoomRegion.
+     * @param {boolean} activate: Boolean to show/hide the ZoomRegion.
      */
     setActive(activate) {
         if (activate == this.isActive())
@@ -854,7 +857,7 @@ var ZoomRegion = class ZoomRegion {
 
     /**
      * isActive:
-     * @returns {bool} Whether this ZoomRegion is active
+     * @returns {boolean} Whether this ZoomRegion is active
      */
     isActive() {
         return this._magView != null;
@@ -993,7 +996,7 @@ var ZoomRegion = class ZoomRegion {
      * setLensMode:
      * Turn lens mode on/off.  In full screen mode, lens mode does nothing since
      * a lens the size of the screen is pointless.
-     * @param {bool} lensMode: Whether lensMode should be active
+     * @param {boolean} lensMode: Whether lensMode should be active
      */
     setLensMode(lensMode) {
         this._lensMode = lensMode;
@@ -1004,7 +1007,7 @@ var ZoomRegion = class ZoomRegion {
     /**
      * isLensMode:
      * Is lens mode on or off?
-     * @returns {bool} The lens mode state.
+     * @returns {boolean} The lens mode state.
      */
     isLensMode() {
         return this._lensMode;
@@ -1014,7 +1017,7 @@ var ZoomRegion = class ZoomRegion {
      * setClampScrollingAtEdges:
      * Stop vs. allow scrolling of the magnified contents when it scroll beyond
      * the edges of the screen.
-     * @param {bool} clamp: Boolean to turn on/off clamping.
+     * @param {boolean} clamp: Boolean to turn on/off clamping.
      */
     setClampScrollingAtEdges(clamp) {
         this._clampScrollingAtEdges = clamp;
@@ -1140,7 +1143,7 @@ var ZoomRegion = class ZoomRegion {
     /**
      * scrollToMousePos:
      * Set the region of interest based on the position of the system pointer.
-     * @returns {bool}: Whether the system mouse pointer is over the
+     * @returns {boolean}: Whether the system mouse pointer is over the
      *     magnified view.
      */
     scrollToMousePos() {
@@ -1200,7 +1203,7 @@ var ZoomRegion = class ZoomRegion {
     /**
      * addCrosshairs:
      * Add crosshairs centered on the magnified mouse.
-     * @param {Crosshairs} crossHairs: Crosshairs instance
+     * @param {Crosshairs["prototype"]} crossHairs: Crosshairs instance
      */
     addCrosshairs(crossHairs) {
         this._crossHairs = crossHairs;
@@ -1214,7 +1217,7 @@ var ZoomRegion = class ZoomRegion {
     /**
      * setInvertLightness:
      * Set whether to invert the lightness of the magnified view.
-     * @param {bool} flag: whether brightness should be inverted
+     * @param {boolean} flag: whether brightness should be inverted
      */
     setInvertLightness(flag) {
         this._invertLightness = flag;
@@ -1225,7 +1228,7 @@ var ZoomRegion = class ZoomRegion {
     /**
      * getInvertLightness:
      * Retrieve whether the lightness is inverted.
-     * @returns {bool} whether brightness should be inverted
+     * @returns {boolean} whether brightness should be inverted
      */
     getInvertLightness() {
         return this._invertLightness;
@@ -1650,7 +1653,7 @@ var ZoomRegion = class ZoomRegion {
     }
 };
 
-var Crosshairs = GObject.registerClass(
+export const Crosshairs = GObject.registerClass(
 class Crosshairs extends Clutter.Actor {
     _init() {
         // Set the group containing the crosshairs to three times the desktop
@@ -1825,7 +1828,7 @@ class Crosshairs extends Clutter.Actor {
      * setClip:
      * Set the width and height of the rectangle that clips the crosshairs at
      * their intersection
-     * @param {number[]} size: Array of [width, height] defining the size
+     * @param {[number, number]} size: Array of [width, height] defining the size
      *     of the clip rectangle.
      */
     setClip(size) {
@@ -1841,12 +1844,16 @@ class Crosshairs extends Clutter.Actor {
         }
     }
 
+    getClip() {
+        return this._clipSize;
+    }
+
     /**
      * reCenter:
      * Reposition the horizontal and vertical hairs such that they cross at
      * the center of crosshairs group.  If called with the dimensions of
      * the clip rectangle, these are used to update the size of the clip.
-     * @param {number[]=} clipSize: If present, the clip's [width, height].
+     * @param {[number, number]} [clipSize]: If present, the clip's [width, height].
      */
     reCenter(clipSize) {
         let [groupWidth, groupHeight] = this.get_size();
@@ -1871,7 +1878,7 @@ class Crosshairs extends Clutter.Actor {
     }
 });
 
-var MagShaderEffects = class MagShaderEffects {
+export class MagShaderEffects {
     constructor(uiGroupClone) {
         this._inverse = new Shell.InvertLightnessEffect();
         this._brightnessContrast = new Clutter.BrightnessContrastEffect();
@@ -1902,7 +1909,7 @@ var MagShaderEffects = class MagShaderEffects {
     /**
      * setInvertLightness:
      * Enable/disable invert lightness effect.
-     * @param {bool} invertFlag: Enabled flag.
+     * @param {boolean} invertFlag: Enabled flag.
      */
     setInvertLightness(invertFlag) {
         this._inverse.set_enabled(invertFlag);
diff --git a/js/ui/main.js b/js/ui/main.js
index 1069674ae9..e848eb0706 100644
--- a/js/ui/main.js
+++ b/js/ui/main.js
@@ -7,47 +7,54 @@
             kbdA11yDialog, introspectService, start, pushModal, popModal,
             activateWindow, createLookingGlass, initializeDeferredWork,
             getThemeStylesheet, setThemeStylesheet */
-
-const { Clutter, Gio, GLib, GObject, Meta, Shell, St } = imports.gi;
-
-const AccessDialog = imports.ui.accessDialog;
-const AudioDeviceSelection = imports.ui.audioDeviceSelection;
-const Components = imports.ui.components;
-const CtrlAltTab = imports.ui.ctrlAltTab;
-const EndSessionDialog = imports.ui.endSessionDialog;
-const ExtensionSystem = imports.ui.extensionSystem;
-const ExtensionDownloader = imports.ui.extensionDownloader;
-const InputMethod = imports.misc.inputMethod;
-const Introspect = imports.misc.introspect;
-const Keyboard = imports.ui.keyboard;
-const MessageTray = imports.ui.messageTray;
-const ModalDialog = imports.ui.modalDialog;
-const OsdWindow = imports.ui.osdWindow;
-const OsdMonitorLabeler = imports.ui.osdMonitorLabeler;
-const Overview = imports.ui.overview;
-const PadOsd = imports.ui.padOsd;
-const Panel = imports.ui.panel;
-const RunDialog = imports.ui.runDialog;
-const WelcomeDialog = imports.ui.welcomeDialog;
-const Layout = imports.ui.layout;
-const LoginManager = imports.misc.loginManager;
-const LookingGlass = imports.ui.lookingGlass;
-const NotificationDaemon = imports.ui.notificationDaemon;
-const WindowAttentionHandler = imports.ui.windowAttentionHandler;
-const ScreenShield = imports.ui.screenShield;
-const Scripting = imports.ui.scripting;
-const SessionMode = imports.ui.sessionMode;
-const ShellDBus = imports.ui.shellDBus;
-const ShellMountOperation = imports.ui.shellMountOperation;
-const WindowManager = imports.ui.windowManager;
-const Magnifier = imports.ui.magnifier;
-const XdndHandler = imports.ui.xdndHandler;
-const KbdA11yDialog = imports.ui.kbdA11yDialog;
-const LocatePointer = imports.ui.locatePointer;
-const PointerA11yTimeout = imports.ui.pointerA11yTimeout;
-const ParentalControlsManager = imports.misc.parentalControlsManager;
+// @ts-check
+
+import Clutter from 'gi://Clutter';
+import Gio from 'gi://Gio';
+import GLib from 'gi://GLib';
+import GObject from 'gi://GObject';
+import Meta from 'gi://Meta';
+import Shell from 'gi://Shell';
+import St from 'gi://St';
+
+import * as AccessDialog from './accessDialog.js';
+import * as AudioDeviceSelection from './audioDeviceSelection.js';
+import * as Components from './components.js';
+import * as CtrlAltTab from './ctrlAltTab.js';
+import * as EndSessionDialog from './endSessionDialog.js';
+import * as InputMethod from '../misc/inputMethod.js';
+import * as Introspect from '../misc/introspect.js';
+import * as Keyboard from './keyboard.js';
+import * as MessageTray from './messageTray.js';
+import * as ModalDialog from './modalDialog.js';
+import * as OsdWindow from './osdWindow.js';
+import * as OsdMonitorLabeler from './osdMonitorLabeler.js';
+import * as Overview from './overview.js';
+import * as PadOsd from './padOsd.js';
+import * as Panel from './panel.js';
+import * as Params from '../misc/params.js';
+import * as RunDialog from './runDialog.js';
+import * as WelcomeDialog from './welcomeDialog.js';
+import * as Layout from './layout.js';
+import * as LoginManager from '../misc/loginManager.js';
+import * as LookingGlass from './lookingGlass.js';
+import * as NotificationDaemon from './notificationDaemon.js';
+import * as WindowAttentionHandler from './windowAttentionHandler.js';
+import * as ScreenShield from './screenShield.js';
+import * as Scripting from './scripting.js';
+import * as SessionMode from './sessionMode.js';
+import * as ShellDBus from './shellDBus.js';
+import * as ShellMountOperation from './shellMountOperation.js';
+import * as WindowManager from './windowManager.js';
+import * as Magnifier from './magnifier.js';
+import * as XdndHandler from './xdndHandler.js';
+import * as KbdA11yDialog from './kbdA11yDialog.js';
+import * as LocatePointer from './locatePointer.js';
+import * as PointerA11yTimeout from './pointerA11yTimeout.js';
+import * as ParentalControlsManager from '../misc/parentalControlsManager.js';
 const Config = imports.misc.config;
-const Util = imports.misc.util;
+import * as Util from '../misc/util.js';
+import * as ExtensionUtils from '../misc/extensionUtils.js';
 
 const WELCOME_DIALOG_LAST_SHOWN_VERSION = 'welcome-dialog-last-shown-version';
 // Make sure to mention the point release, otherwise it will show every time
@@ -56,777 +63,823 @@ const WELCOME_DIALOG_LAST_TOUR_CHANGE = '40.beta';
 const LOG_DOMAIN = 'GNOME Shell';
 const GNOMESHELL_STARTED_MESSAGE_ID = 'f3ea493c22934e26811cd62abe8e203a';
 
-var componentManager = null;
-var extensionManager = null;
-var panel = null;
-var overview = null;
-var runDialog = null;
-var lookingGlass = null;
-var welcomeDialog = null;
-var wm = null;
-var messageTray = null;
-var screenShield = null;
-var notificationDaemon = null;
-var windowAttentionHandler = null;
-var ctrlAltTabManager = null;
-var padOsdService = null;
-var osdWindowManager = null;
-var osdMonitorLabeler = null;
-var sessionMode = null;
-var shellAccessDialogDBusService = null;
-var shellAudioSelectionDBusService = null;
-var shellDBusService = null;
-var shellMountOpDBusService = null;
-var screenSaverDBus = null;
-var modalCount = 0;
-var actionMode = Shell.ActionMode.NONE;
-var modalActorFocusStack = [];
-var uiGroup = null;
-var magnifier = null;
-var xdndHandler = null;
-var keyboard = null;
-var layoutManager = null;
-var kbdA11yDialog = null;
-var inputMethod = null;
-var introspectService = null;
-var locatePointer = null;
-let _startDate;
-let _defaultCssStylesheet = null;
-let _cssStylesheet = null;
-let _themeResource = null;
-let _oskResource = null;
-
-Gio._promisify(Gio._LocalFilePrototype, 'delete_async', 'delete_finish');
-Gio._promisify(Gio._LocalFilePrototype, 'touch_async', 'touch_finish');
-
-let _remoteAccessInhibited = false;
-
-function _sessionUpdated() {
-    if (sessionMode.isPrimary)
-        _loadDefaultStylesheet();
-
-    wm.allowKeybinding('overlay-key', Shell.ActionMode.NORMAL |
-                                      Shell.ActionMode.OVERVIEW);
-
-    wm.allowKeybinding('locate-pointer-key', Shell.ActionMode.ALL);
-
-    wm.setCustomKeybindingHandler('panel-run-dialog',
-                                  Shell.ActionMode.NORMAL |
-                                  Shell.ActionMode.OVERVIEW,
-                                  sessionMode.hasRunDialog ? openRunDialog : null);
-
-    if (!sessionMode.hasRunDialog) {
-        if (runDialog)
-            runDialog.close();
-        if (lookingGlass)
-            lookingGlass.close();
-        if (welcomeDialog)
-            welcomeDialog.close();
-    }
-
-    let remoteAccessController = global.backend.get_remote_access_controller();
-    if (remoteAccessController) {
-        if (sessionMode.allowScreencast && _remoteAccessInhibited) {
-            remoteAccessController.uninhibit_remote_access();
-            _remoteAccessInhibited = false;
-        } else if (!sessionMode.allowScreencast && !_remoteAccessInhibited) {
-            remoteAccessController.inhibit_remote_access();
-            _remoteAccessInhibited = true;
+export class Main {
+    componentManager = null;
+    extensionManager = null;
+    panel = null;
+    overview = null;
+    runDialog = null;
+    lookingGlass = null;
+    welcomeDialog = null;
+    /** @type {WindowManager.WindowManager} */
+    wm = null;
+    messageTray = null;
+    screenShield = null;
+    notificationDaemon = null;
+    windowAttentionHandler = null;
+    /** @type {CtrlAltTab.CtrlAltTabManager} */
+    ctrlAltTabManager = null;
+    padOsdService = null;
+    osdWindowManager = null;
+    osdMonitorLabeler = null;
+    sessionMode = null;
+    shellAccessDialogDBusService = null;
+    shellAudioSelectionDBusService = null;
+    shellDBusService = null;
+    shellMountOpDBusService = null;
+    screenSaverDBus = null;
+    modalCount = 0;
+    actionMode = Shell.ActionMode.NONE;
+    modalActorFocusStack = [];
+    uiGroup = null;
+    /** @type {Magnifier.Magnifier} */
+    magnifier = null;
+    /** @type {XdndHandler.XdndHandler} */
+    xdndHandler = null;
+    keyboard = null;
+    /** @type {Layout.LayoutManager["prototype"]} */
+    layoutManager = null;
+    kbdA11yDialog = null;
+    inputMethod = null;
+    introspectService = null;
+    locatePointer = null;
+    _startDate;
+    _defaultCssStylesheet = null;
+    _cssStylesheet = null;
+    _themeResource = null;
+    _oskResource = null;
+    
+    _remoteAccessInhibited = false;
+
+    _sessionUpdated() {
+        const { wm, sessionMode, welcomeDialog, overview, runDialog, lookingGlass } = this;
+    
+        if (sessionMode.isPrimary)
+            this._loadDefaultStylesheet();
+    
+    
+        wm.setCustomKeybindingHandler('panel-main-menu',
+            Shell.ActionMode.NORMAL |
+            Shell.ActionMode.OVERVIEW,
+            sessionMode.hasOverview ? overview.toggle.bind(overview) : null);
+        wm.allowKeybinding('overlay-key', Shell.ActionMode.NORMAL |
+            Shell.ActionMode.OVERVIEW);
+    
+        wm.allowKeybinding('locate-pointer-key', Shell.ActionMode.ALL);
+    
+        wm.setCustomKeybindingHandler('panel-run-dialog',
+            Shell.ActionMode.NORMAL |
+            Shell.ActionMode.OVERVIEW,
+            sessionMode.hasRunDialog ? this.openRunDialog : null);
+    
+        if (!sessionMode.hasRunDialog) {
+            if (runDialog)
+                runDialog.close();
+            if (lookingGlass)
+                lookingGlass.close();
+            if (welcomeDialog)
+                welcomeDialog.close();
+        }
+    
+        let remoteAccessController = global.backend.get_remote_access_controller();
+        if (remoteAccessController) {
+            if (sessionMode.allowScreencast && this._remoteAccessInhibited) {
+                remoteAccessController.uninhibit_remote_access();
+                this._remoteAccessInhibited = false;
+            } else if (!sessionMode.allowScreencast && !this._remoteAccessInhibited) {
+                remoteAccessController.inhibit_remote_access();
+                this._remoteAccessInhibited = true;
+            }
         }
-    }
-}
-
-function start() {
-    // These are here so we don't break compatibility.
-    global.logError = globalThis.log;
-    global.log = globalThis.log;
-
-    // Chain up async errors reported from C
-    global.connect('notify-error', (global, msg, detail) => {
-        notifyError(msg, detail);
-    });
-
-    let currentDesktop = GLib.getenv('XDG_CURRENT_DESKTOP');
-    if (!currentDesktop || !currentDesktop.split(':').includes('GNOME'))
-        Gio.DesktopAppInfo.set_desktop_env('GNOME');
-
-    sessionMode = new SessionMode.SessionMode();
-    sessionMode.connect('updated', _sessionUpdated);
-
-    St.Settings.get().connect('notify::gtk-theme', _loadDefaultStylesheet);
-
-    // Initialize ParentalControlsManager before the UI
-    ParentalControlsManager.getDefault();
-
-    _initializeUI();
-
-    shellAccessDialogDBusService = new AccessDialog.AccessDialogDBus();
-    shellAudioSelectionDBusService = new AudioDeviceSelection.AudioDeviceSelectionDBus();
-    shellDBusService = new ShellDBus.GnomeShell();
-    shellMountOpDBusService = new ShellMountOperation.GnomeShellMountOpHandler();
-
-    const watchId = Gio.DBus.session.watch_name('org.gnome.Shell.Notifications',
-        Gio.BusNameWatcherFlags.AUTO_START,
-        bus => bus.unwatch_name(watchId),
-        bus => bus.unwatch_name(watchId));
-
-    _sessionUpdated();
-}
-
-function _initializeUI() {
-    // Ensure ShellWindowTracker and ShellAppUsage are initialized; this will
-    // also initialize ShellAppSystem first. ShellAppSystem
-    // needs to load all the .desktop files, and ShellWindowTracker
-    // will use those to associate with windows. Right now
-    // the Monitor doesn't listen for installed app changes
-    // and recalculate application associations, so to avoid
-    // races for now we initialize it here. It's better to
-    // be predictable anyways.
-    Shell.WindowTracker.get_default();
-    Shell.AppUsage.get_default();
-
-    reloadThemeResource();
-    _loadOskLayouts();
-    _loadDefaultStylesheet();
-
-    new AnimationsSettings();
-
-    // Setup the stage hierarchy early
-    layoutManager = new Layout.LayoutManager();
-
-    // Various parts of the codebase still refer to Main.uiGroup
-    // instead of using the layoutManager. This keeps that code
-    // working until it's updated.
-    uiGroup = layoutManager.uiGroup;
-
-    padOsdService = new PadOsd.PadOsdService();
-    xdndHandler = new XdndHandler.XdndHandler();
-    ctrlAltTabManager = new CtrlAltTab.CtrlAltTabManager();
-    osdWindowManager = new OsdWindow.OsdWindowManager();
-    osdMonitorLabeler = new OsdMonitorLabeler.OsdMonitorLabeler();
-    overview = new Overview.Overview();
-    kbdA11yDialog = new KbdA11yDialog.KbdA11yDialog();
-    wm = new WindowManager.WindowManager();
-    magnifier = new Magnifier.Magnifier();
-    locatePointer = new LocatePointer.LocatePointer();
-
-    if (LoginManager.canLock())
-        screenShield = new ScreenShield.ScreenShield();
-
-    inputMethod = new InputMethod.InputMethod();
-    Clutter.get_default_backend().set_input_method(inputMethod);
-
-    messageTray = new MessageTray.MessageTray();
-    panel = new Panel.Panel();
-    keyboard = new Keyboard.KeyboardManager();
-    notificationDaemon = new NotificationDaemon.NotificationDaemon();
-    windowAttentionHandler = new WindowAttentionHandler.WindowAttentionHandler();
-    componentManager = new Components.ComponentManager();
-
-    introspectService = new Introspect.IntrospectService();
-
-    layoutManager.init();
-    overview.init();
-
-    new PointerA11yTimeout.PointerA11yTimeout();
-
-    global.connect('locate-pointer', () => {
-        locatePointer.show();
-    });
-
-    global.display.connect('show-restart-message', (display, message) => {
-        showRestartMessage(message);
-        return true;
-    });
-
-    global.display.connect('restart', () => {
-        global.reexec_self();
-        return true;
-    });
-
-    global.display.connect('gl-video-memory-purged', loadTheme);
-
-    // Provide the bus object for gnome-session to
-    // initiate logouts.
-    EndSessionDialog.init();
-
-    // We're ready for the session manager to move to the next phase
-    GLib.idle_add(GLib.PRIORITY_DEFAULT, () => {
-        Shell.util_sd_notify();
-        global.context.notify_ready();
-        return GLib.SOURCE_REMOVE;
-    });
-
-    _startDate = new Date();
-
-    ExtensionDownloader.init();
-    extensionManager = new ExtensionSystem.ExtensionManager();
-    extensionManager.init();
 
-    if (sessionMode.isGreeter && screenShield) {
-        layoutManager.connect('startup-prepared', () => {
-            screenShield.showDialog();
+        log('session updated...');
+    }
+    
+    start() {
+        // These are here so we don't break compatibility.
+        global.logError = globalThis.log;
+        global.log = globalThis.log;
+    
+        // Chain up async errors reported from C
+        global.connect('notify-error', (global, msg, detail) => {
+            this.notifyError(msg, detail);
         });
+    
+        let currentDesktop = GLib.getenv('XDG_CURRENT_DESKTOP');
+        if (!currentDesktop || !currentDesktop.split(':').includes('GNOME'))
+            Gio.DesktopAppInfo.set_desktop_env('GNOME');
+    
+        this.sessionMode = new SessionMode.SessionMode();
+        this.sessionMode.connect('updated', this._sessionUpdated);
+    
+        St.Settings.get().connect('notify::gtk-theme', this._loadDefaultStylesheet);
+    
+        // Initialize ParentalControlsManager before the UI
+        ParentalControlsManager.getDefault();
+    
+        this._initializeUI();
+    
+        this.shellAccessDialogDBusService = new AccessDialog.AccessDialogDBus();
+        this.shellAudioSelectionDBusService = new AudioDeviceSelection.AudioDeviceSelectionDBus();
+        this.shellDBusService = new ShellDBus.GnomeShell();
+        this.shellMountOpDBusService = new ShellMountOperation.GnomeShellMountOpHandler();
+    
+        const watchId = Gio.DBus.session.watch_name('org.gnome.Shell.Notifications',
+            Gio.BusNameWatcherFlags.AUTO_START,
+            bus => bus.unwatch_name(watchId),
+            bus => bus.unwatch_name(watchId));
+    
+        this._sessionUpdated();
+    
+        log("Started...");
     }
-
-    layoutManager.connect('startup-complete', () => {
-        if (actionMode == Shell.ActionMode.NONE)
-            actionMode = Shell.ActionMode.NORMAL;
-
-        if (screenShield)
-            screenShield.lockIfWasLocked();
-
-        if (sessionMode.currentMode != 'gdm' &&
-            sessionMode.currentMode != 'initial-setup') {
-            GLib.log_structured(LOG_DOMAIN, GLib.LogLevelFlags.LEVEL_MESSAGE, {
-                'MESSAGE': 'GNOME Shell started at %s'.format(_startDate),
-                'MESSAGE_ID': GNOMESHELL_STARTED_MESSAGE_ID,
+    
+    _initializeUI() {
+        // Ensure ShellWindowTracker and ShellAppUsage are initialized; this will
+        // also initialize ShellAppSystem first. ShellAppSystem
+        // needs to load all the .desktop files, and ShellWindowTracker
+        // will use those to associate with windows. Right now
+        // the Monitor doesn't listen for installed app changes
+        // and recalculate application associations, so to avoid
+        // races for now we initialize it here. It's better to
+        // be predictable anyways.
+        Shell.WindowTracker.get_default();
+        Shell.AppUsage.get_default();
+    
+        this.reloadThemeResource();
+        this._loadOskLayouts();
+        this._loadDefaultStylesheet();
+    
+        new AnimationsSettings();
+    
+        // Setup the stage hierarchy early
+        this.layoutManager = new Layout.LayoutManager();
+    
+        // Various parts of the codebase still refer to Main.uiGroup
+        // instead of using the layoutManager. This keeps that code
+        // working until it's updated.
+        this.uiGroup = this.layoutManager.uiGroup;
+    
+        this.padOsdService = new PadOsd.PadOsdService();
+        this.xdndHandler = new XdndHandler.XdndHandler();
+        this.ctrlAltTabManager = new CtrlAltTab.CtrlAltTabManager();
+        this.osdWindowManager = new OsdWindow.OsdWindowManager();
+        this.osdMonitorLabeler = new OsdMonitorLabeler.OsdMonitorLabeler();
+        this.overview = new Overview.Overview();
+        this.kbdA11yDialog = new KbdA11yDialog.KbdA11yDialog();
+        this.wm = new WindowManager.WindowManager();
+        this.magnifier = new Magnifier.Magnifier();
+        this.locatePointer = new LocatePointer.LocatePointer();
+    
+        if (LoginManager.canLock())
+            this.screenShield = new ScreenShield.ScreenShield();
+    
+        this.inputMethod = new InputMethod.InputMethod();
+        Clutter.get_default_backend().set_input_method(this.inputMethod);
+    
+        this.messageTray = new MessageTray.MessageTray();
+        this.panel = new Panel.Panel();
+        this.keyboard = new Keyboard.KeyboardManager();
+        this.notificationDaemon = new NotificationDaemon.NotificationDaemon();
+        this.windowAttentionHandler = new WindowAttentionHandler.WindowAttentionHandler();
+        this.componentManager = new Components.ComponentManager();
+    
+        this.introspectService = new Introspect.IntrospectService();
+    
+        this.layoutManager.init();
+        this.overview.init();
+    
+        new PointerA11yTimeout.PointerA11yTimeout();
+    
+        global.connect('locate-pointer', () => {
+            this.locatePointer.show();
+        });
+    
+        global.display.connect('show-restart-message', (display, message) => {
+            showRestartMessage(message);
+            return true;
+        });
+    
+        global.display.connect('restart', () => {
+            global.reexec_self();
+            return true;
+        });
+    
+        global.display.connect('gl-video-memory-purged', this.loadTheme);
+    
+        // Provide the bus object for gnome-session to
+        // initiate logouts.
+        EndSessionDialog.init();
+    
+        // We're ready for the session manager to move to the next phase
+        GLib.idle_add(GLib.PRIORITY_DEFAULT, () => {
+            log('>>>>');
+            Shell.util_sd_notify();
+            global.context.notify_ready();
+            log('<<<<<')
+            return GLib.SOURCE_REMOVE;
+        });
+    
+        this._startDate = new Date();
+    
+        // ExtensionDownloader.init();
+        // this.extensionManager = new ExtensionSystem.ExtensionManager();
+        // this.extensionManager.init();
+    
+        if (this.sessionMode.isGreeter && this.screenShield) {
+            this.layoutManager.connect('startup-prepared', () => {
+                log('Startup Prepared!');
+                this.screenShield.showDialog();
             });
         }
-
-        let credentials = new Gio.Credentials();
-        if (credentials.get_unix_user() === 0) {
-            notify(_('Logged in as a privileged user'),
-                   _('Running a session as a privileged user should be avoided for security reasons. If 
possible, you should log in as a normal user.'));
-        } else if (sessionMode.showWelcomeDialog) {
-            _handleShowWelcomeScreen();
-        }
-
-        if (sessionMode.currentMode !== 'gdm' &&
-            sessionMode.currentMode !== 'initial-setup')
-            _handleLockScreenWarning();
-
-        LoginManager.registerSessionWithGDM();
-
-        let perfModuleName = GLib.getenv("SHELL_PERF_MODULE");
-        if (perfModuleName) {
-            let perfOutput = GLib.getenv("SHELL_PERF_OUTPUT");
-            let module = eval('imports.perf.%s;'.format(perfModuleName));
-            Scripting.runPerfScript(module, perfOutput);
+    
+        this.layoutManager.connect('startup-complete', () => {
+            log('Startup Complete!');
+            if (this.actionMode == Shell.ActionMode.NONE)
+                this.actionMode = Shell.ActionMode.NORMAL;
+    
+            if (this.screenShield)
+                this.screenShield.lockIfWasLocked();
+    
+            if (this.sessionMode.currentMode != 'gdm' &&
+                this.sessionMode.currentMode != 'initial-setup') {
+                GLib.log_structured(LOG_DOMAIN, GLib.LogLevelFlags.LEVEL_MESSAGE, {
+                    'MESSAGE': 'GNOME Shell started at %s'.format(this._startDate),
+                    'MESSAGE_ID': GNOMESHELL_STARTED_MESSAGE_ID,
+                });
+            }
+    
+            let credentials = new Gio.Credentials();
+            if (credentials.get_unix_user() === 0) {
+                this.notify(_('Logged in as a privileged user'),
+                       _('Running a session as a privileged user should be avoided for security reasons. If 
possible, you should log in as a normal user.'));
+            } else if (this.sessionMode.showWelcomeDialog) {
+                this._handleShowWelcomeScreen();
+            }
+    
+            if (this.sessionMode.currentMode !== 'gdm' &&
+                this.sessionMode.currentMode !== 'initial-setup')
+                this._handleLockScreenWarning();
+    
+            LoginManager.registerSessionWithGDM();
+    
+            let perfModuleName = GLib.getenv("SHELL_PERF_MODULE");
+            if (perfModuleName) {
+                let perfOutput = GLib.getenv("SHELL_PERF_OUTPUT");
+                let module = eval('imports.perf.%s;'.format(perfModuleName));
+                Scripting.runPerfScript(module, perfOutput);
+            }
+        });
+    
+        log('done init...');
+    }
+    
+    _handleShowWelcomeScreen() {
+        const lastShownVersion = global.settings.get_string(WELCOME_DIALOG_LAST_SHOWN_VERSION);
+        if (Util.GNOMEversionCompare(WELCOME_DIALOG_LAST_TOUR_CHANGE, lastShownVersion) > 0) {
+            this.openWelcomeDialog();
+            global.settings.set_string(WELCOME_DIALOG_LAST_SHOWN_VERSION, Config.PACKAGE_VERSION);
         }
-    });
-}
-
-function _handleShowWelcomeScreen() {
-    const lastShownVersion = global.settings.get_string(WELCOME_DIALOG_LAST_SHOWN_VERSION);
-    if (Util.GNOMEversionCompare(WELCOME_DIALOG_LAST_TOUR_CHANGE, lastShownVersion) > 0) {
-        openWelcomeDialog();
-        global.settings.set_string(WELCOME_DIALOG_LAST_SHOWN_VERSION, Config.PACKAGE_VERSION);
     }
-}
 
-async function _handleLockScreenWarning() {
-    const path = '%s/lock-warning-shown'.format(global.userdatadir);
-    const file = Gio.File.new_for_path(path);
-
-    const hasLockScreen = screenShield !== null;
-    if (hasLockScreen) {
-        try {
-            await file.delete_async(0, null);
-        } catch (e) {
-            if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_FOUND))
+    async _handleLockScreenWarning() {
+        const path = '%s/lock-warning-shown'.format(global.userdatadir);
+        const file = Gio.File.new_for_path(path);
+
+        const hasLockScreen = this.screenShield !== null;
+        if (hasLockScreen) {
+            try {
+                await file.delete_async(0, null);
+            } catch (e) {
+                if (!e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_FOUND))
+                    logError(e);
+            }
+        } else {
+            try {
+                if (!await file.touch_async())
+                    return;
+            } catch (e) {
                 logError(e);
-        }
-    } else {
-        try {
-            if (!await file.touch_async())
-                return;
-        } catch (e) {
-            logError(e);
-        }
+            }
 
-        notify(
-            _('Screen Lock disabled'),
-            _('Screen Locking requires the GNOME display manager.'));
+            this.notify(
+                _('Screen Lock disabled'),
+                _('Screen Locking requires the GNOME display manager.'));
+        }
     }
-}
+    _getStylesheet(name) {
+        let stylesheet;
 
-function _getStylesheet(name) {
-    let stylesheet;
+        stylesheet = Gio.File.new_for_uri('resource:///org/gnome/shell/theme/%s'.format(name));
+        if (stylesheet.query_exists(null))
+            return stylesheet;
 
-    stylesheet = Gio.File.new_for_uri('resource:///org/gnome/shell/theme/%s'.format(name));
-    if (stylesheet.query_exists(null))
-        return stylesheet;
+        let dataDirs = GLib.get_system_data_dirs();
+        for (let i = 0; i < dataDirs.length; i++) {
+            let path = GLib.build_filenamev([dataDirs[i], 'gnome-shell', 'theme', name]);
+            stylesheet = Gio.file_new_for_path(path);
+            if (stylesheet.query_exists(null))
+                return stylesheet;
+        }
 
-    let dataDirs = GLib.get_system_data_dirs();
-    for (let i = 0; i < dataDirs.length; i++) {
-        let path = GLib.build_filenamev([dataDirs[i], 'gnome-shell', 'theme', name]);
-        stylesheet = Gio.file_new_for_path(path);
+        stylesheet = Gio.File.new_for_path('%s/theme/%s'.format(global.datadir, name));
         if (stylesheet.query_exists(null))
             return stylesheet;
-    }
 
-    stylesheet = Gio.File.new_for_path('%s/theme/%s'.format(global.datadir, name));
-    if (stylesheet.query_exists(null))
-        return stylesheet;
+        return null;
+    }
 
-    return null;
-}
+    _getDefaultStylesheet() {
+        let stylesheet = null;
+        let name = this.sessionMode.stylesheetName;
 
-function _getDefaultStylesheet() {
-    let stylesheet = null;
-    let name = sessionMode.stylesheetName;
+        // Look for a high-contrast variant first when using GTK+'s HighContrast
+        // theme
+        if (St.Settings.get().gtk_theme == 'HighContrast')
+            stylesheet = this._getStylesheet(name.replace('.css', '-high-contrast.css'));
 
-    // Look for a high-contrast variant first when using GTK+'s HighContrast
-    // theme
-    if (St.Settings.get().gtk_theme == 'HighContrast')
-        stylesheet = _getStylesheet(name.replace('.css', '-high-contrast.css'));
+        if (stylesheet == null)
+            stylesheet = this._getStylesheet(this.sessionMode.stylesheetName);
 
-    if (stylesheet == null)
-        stylesheet = _getStylesheet(sessionMode.stylesheetName);
+        return stylesheet;
+    }
 
-    return stylesheet;
-}
+    _loadDefaultStylesheet() {
+        let stylesheet = this._getDefaultStylesheet();
+        if (this._defaultCssStylesheet && this._defaultCssStylesheet.equal(stylesheet))
+            return;
 
-function _loadDefaultStylesheet() {
-    let stylesheet = _getDefaultStylesheet();
-    if (_defaultCssStylesheet && _defaultCssStylesheet.equal(stylesheet))
-        return;
+        this._defaultCssStylesheet = stylesheet;
+        this.loadTheme();
+    }
 
-    _defaultCssStylesheet = stylesheet;
-    loadTheme();
-}
+    /**
+     * getThemeStylesheet:
+     *
+     * Get the theme CSS file that the shell will load
+     *
+     * @returns {?Gio.File}: A #GFile that contains the theme CSS,
+     *          null if using the default
+     */
+    getThemeStylesheet() {
+        return this._cssStylesheet;
+    }
 
-/**
- * getThemeStylesheet:
- *
- * Get the theme CSS file that the shell will load
- *
- * @returns {?Gio.File}: A #GFile that contains the theme CSS,
- *          null if using the default
- */
-function getThemeStylesheet() {
-    return _cssStylesheet;
-}
 
-/**
- * setThemeStylesheet:
+    /**
+     * setThemeStylesheet:
  * @param {string=} cssStylesheet: A file path that contains the theme CSS,
  *     set it to null to use the default
  *
  * Set the theme CSS file that the shell will load
  */
-function setThemeStylesheet(cssStylesheet) {
-    _cssStylesheet = cssStylesheet ? Gio.File.new_for_path(cssStylesheet) : null;
-}
+    setThemeStylesheet(cssStylesheet) {
+        this._cssStylesheet = cssStylesheet ? Gio.File.new_for_path(cssStylesheet) : null;
+    }
 
-function reloadThemeResource() {
-    if (_themeResource)
-        _themeResource._unregister();
+    reloadThemeResource() {
+        if (this._themeResource)
+            this._themeResource._unregister();
 
-    _themeResource = Gio.Resource.load('%s/%s'.format(global.datadir,
-        sessionMode.themeResourceName));
-    _themeResource._register();
-}
+        this._themeResource = Gio.Resource.load('%s/%s'.format(global.datadir,
+            this.sessionMode.themeResourceName));
+        this._themeResource._register();
+    }
 
-function _loadOskLayouts() {
-    _oskResource = Gio.Resource.load('%s/gnome-shell-osk-layouts.gresource'.format(global.datadir));
-    _oskResource._register();
-}
+    _loadOskLayouts() {
+        this._oskResource = Gio.Resource.load('%s/gnome-shell-osk-layouts.gresource'.format(global.datadir));
+        this._oskResource._register();
+    }
 
-/**
- * loadTheme:
- *
- * Reloads the theme CSS file
- */
-function loadTheme() {
-    let themeContext = St.ThemeContext.get_for_stage(global.stage);
-    let previousTheme = themeContext.get_theme();
 
-    let theme = new St.Theme({
-        application_stylesheet: _cssStylesheet,
-        default_stylesheet: _defaultCssStylesheet,
-    });
+    /**
+     * loadTheme:
+     *
+     * Reloads the theme CSS file
+     */
+    loadTheme() {
+        let themeContext = St.ThemeContext.get_for_stage(global.stage);
+        let previousTheme = themeContext.get_theme();
 
-    if (theme.default_stylesheet == null)
-        throw new Error("No valid stylesheet found for '%s'".format(sessionMode.stylesheetName));
+        let theme = new St.Theme({
+            application_stylesheet: this._cssStylesheet,
+            default_stylesheet: this._defaultCssStylesheet,
+        });
 
-    if (previousTheme) {
-        let customStylesheets = previousTheme.get_custom_stylesheets();
+        if (theme.default_stylesheet == null)
+            throw new Error("No valid stylesheet found for '%s'".format(this.sessionMode.stylesheetName));
 
-        for (let i = 0; i < customStylesheets.length; i++)
-            theme.load_stylesheet(customStylesheets[i]);
-    }
+        if (previousTheme) {
+            let customStylesheets = previousTheme.get_custom_stylesheets();
 
-    themeContext.set_theme(theme);
-}
+            for (let i = 0; i < customStylesheets.length; i++)
+                theme.load_stylesheet(customStylesheets[i]);
+        }
 
-/**
- * notify:
- * @param {string} msg: A message
- * @param {string} details: Additional information
- */
-function notify(msg, details) {
-    let source = new MessageTray.SystemNotificationSource();
-    messageTray.add(source);
-    let notification = new MessageTray.Notification(source, msg, details);
-    notification.setTransient(true);
-    source.showNotification(notification);
-}
+        themeContext.set_theme(theme);
+    }
 
-/**
- * notifyError:
- * @param {string} msg: An error message
- * @param {string} details: Additional information
- *
- * See shell_global_notify_problem().
- */
-function notifyError(msg, details) {
-    // Also print to stderr so it's logged somewhere
-    if (details)
-        log('error: %s: %s'.format(msg, details));
-    else
-        log('error: %s'.format(msg));
-
-    notify(msg, details);
-}
+    /**
+     * notify:
+     * @param {string} msg: A message
+     * @param {string} details: Additional information
+     */
+    notify(msg, details) {
+        let source = new MessageTray.SystemNotificationSource();
+        this.messageTray.add(source);
+        let notification = new MessageTray.Notification(source, msg, details);
+        notification.setTransient(true);
+        source.showNotification(notification);
+    }
 
-function _findModal(actor) {
-    for (let i = 0; i < modalActorFocusStack.length; i++) {
-        if (modalActorFocusStack[i].actor == actor)
-            return i;
+    /**
+     * notifyError:
+     * @param {string} msg: An error message
+     * @param {string} details: Additional information
+     *
+     * See shell_global_notify_problem().
+     */
+    notifyError(msg, details) {
+        // Also print to stderr so it's logged somewhere
+        if (details)
+            log('error: %s: %s'.format(msg, details));
+        else
+            log('error: %s'.format(msg));
+
+        this.notify(msg, details);
     }
-    return -1;
-}
 
-/**
- * pushModal:
- * @param {Clutter.Actor} actor: actor which will be given keyboard focus
- * @param {Object=} params: optional parameters
- *
- * Ensure we are in a mode where all keyboard and mouse input goes to
- * the stage, and focus @actor. Multiple calls to this function act in
- * a stacking fashion; the effect will be undone when an equal number
- * of popModal() invocations have been made.
- *
- * Next, record the current Clutter keyboard focus on a stack. If the
- * modal stack returns to this actor, reset the focus to the actor
- * which was focused at the time pushModal() was invoked.
- *
- * @params may be used to provide the following parameters:
- *  - timestamp: used to associate the call with a specific user initiated
- *               event. If not provided then the value of
- *               global.get_current_time() is assumed.
- *
- *  - options: Meta.ModalOptions flags to indicate that the pointer is
- *             already grabbed
- *
- *  - actionMode: used to set the current Shell.ActionMode to filter
- *                global keybindings; the default of NONE will filter
- *                out all keybindings
- *
- * @returns {bool}: true iff we successfully acquired a grab or already had one
- */
-function pushModal(actor, params = {}) {
-    const {
-        timestamp = global.get_current_time(),
-        options = 0,
-        actionMode: modalActionMode = Shell.ActionMode.NONE,
-    } = params;
-
-    if (modalCount == 0) {
-        if (!global.begin_modal(timestamp, options)) {
-            log('pushModal: invocation of begin_modal failed');
-            return false;
+    _findModal(actor) {
+        for (let i = 0; i < this.modalActorFocusStack.length; i++) {
+            if (this.modalActorFocusStack[i].actor == actor)
+                return i;
         }
-        Meta.disable_unredirect_for_display(global.display);
+        return -1;
     }
 
-    modalCount += 1;
-    let actorDestroyId = actor.connect('destroy', () => {
-        let index = _findModal(actor);
-        if (index >= 0)
-            popModal(actor);
-    });
+    /**
+     * pushModal:
+     * @param {Clutter.Actor} actor: actor which will be given keyboard focus
+     * @param {Object=} params: optional parameters
+     *
+     * Ensure we are in a mode where all keyboard and mouse input goes to
+     * the stage, and focus @actor. Multiple calls to this function act in
+     * a stacking fashion; the effect will be undone when an equal number
+     * of popModal() invocations have been made.
+     *
+     * Next, record the current Clutter keyboard focus on a stack. If the
+     * modal stack returns to this actor, reset the focus to the actor
+     * which was focused at the time pushModal() was invoked.
+     *
+     * @params may be used to provide the following parameters:
+     *  - timestamp: used to associate the call with a specific user initiated
+     *               event. If not provided then the value of
+     *               global.get_current_time() is assumed.
+     *
+     *  - options: Meta.ModalOptions flags to indicate that the pointer is
+     *             already grabbed
+     *
+     *  - actionMode: used to set the current Shell.ActionMode to filter
+     *                global keybindings; the default of NONE will filter
+     *                out all keybindings
+     *
+     * @returns {boolean}: true iff we successfully acquired a grab or already had one
+     */
+    pushModal(actor, params) {
+        params = Params.parse(params, {
+            timestamp: global.get_current_time(),
+            options: 0,
+            actionMode: Shell.ActionMode.NONE
+        });
 
-    let prevFocus = global.stage.get_key_focus();
-    let prevFocusDestroyId;
-    if (prevFocus != null) {
-        prevFocusDestroyId = prevFocus.connect('destroy', () => {
-            const index = modalActorFocusStack.findIndex(
-                record => record.prevFocus === prevFocus);
+        if (this.modalCount == 0) {
+            if (!global.begin_modal(params.timestamp, params.options)) {
+                log('pushModal: invocation of begin_modal failed');
+                return false;
+            }
+            Meta.disable_unredirect_for_display(global.display);
+        }
 
+        this.modalCount += 1;
+        let actorDestroyId = actor.connect('destroy', () => {
+            let index = this._findModal(actor);
             if (index >= 0)
-                modalActorFocusStack[index].prevFocus = null;
+                this.popModal(actor);
         });
-    }
-    modalActorFocusStack.push({ actor,
-                                destroyId: actorDestroyId,
-                                prevFocus,
-                                prevFocusDestroyId,
-                                actionMode });
-
-    actionMode = modalActionMode;
-    global.stage.set_key_focus(actor);
-    return true;
-}
 
-/**
- * popModal:
- * @param {Clutter.Actor} actor: the actor passed to original invocation
- *     of pushModal()
- * @param {number=} timestamp: optional timestamp
- *
- * Reverse the effect of pushModal(). If this invocation is undoing
- * the topmost invocation, then the focus will be restored to the
- * previous focus at the time when pushModal() was invoked.
- *
- * @timestamp is optionally used to associate the call with a specific user
- * initiated event. If not provided then the value of
- * global.get_current_time() is assumed.
- */
-function popModal(actor, timestamp) {
-    if (timestamp == undefined)
-        timestamp = global.get_current_time();
+        let prevFocus = global.stage.get_key_focus();
+        let prevFocusDestroyId;
+        if (prevFocus != null) {
+            prevFocusDestroyId = prevFocus.connect('destroy', () => {
+                const index = this.modalActorFocusStack.findIndex(
+                    record => record.prevFocus === prevFocus);
 
-    let focusIndex = _findModal(actor);
-    if (focusIndex < 0) {
-        global.stage.set_key_focus(null);
-        global.end_modal(timestamp);
-        actionMode = Shell.ActionMode.NORMAL;
+                if (index >= 0)
+                    this.modalActorFocusStack[index].prevFocus = null;
+            });
+        }
+        this.modalActorFocusStack.push({
+            actor,
+            destroyId: actorDestroyId,
+            prevFocus,
+            prevFocusDestroyId,
+            actionMode: this.actionMode
+        });
 
-        throw new Error('incorrect pop');
+        this.actionMode = params.actionMode;
+        global.stage.set_key_focus(actor);
+        return true;
     }
 
-    modalCount -= 1;
-
-    let record = modalActorFocusStack[focusIndex];
-    record.actor.disconnect(record.destroyId);
-
-    if (focusIndex == modalActorFocusStack.length - 1) {
-        if (record.prevFocus)
-            record.prevFocus.disconnect(record.prevFocusDestroyId);
-        actionMode = record.actionMode;
-        global.stage.set_key_focus(record.prevFocus);
-    } else {
-        // If we have:
-        //     global.stage.set_focus(a);
-        //     Main.pushModal(b);
-        //     Main.pushModal(c);
-        //     Main.pushModal(d);
-        //
-        // then we have the stack:
-        //     [{ prevFocus: a, actor: b },
-        //      { prevFocus: b, actor: c },
-        //      { prevFocus: c, actor: d }]
-        //
-        // When actor c is destroyed/popped, if we only simply remove the
-        // record, then the focus stack will be [a, c], rather than the correct
-        // [a, b]. Shift the focus stack up before removing the record to ensure
-        // that we get the correct result.
-        let t = modalActorFocusStack[modalActorFocusStack.length - 1];
-        if (t.prevFocus)
-            t.prevFocus.disconnect(t.prevFocusDestroyId);
-        // Remove from the middle, shift the focus chain up
-        for (let i = modalActorFocusStack.length - 1; i > focusIndex; i--) {
-            modalActorFocusStack[i].prevFocus = modalActorFocusStack[i - 1].prevFocus;
-            modalActorFocusStack[i].prevFocusDestroyId = modalActorFocusStack[i - 1].prevFocusDestroyId;
-            modalActorFocusStack[i].actionMode = modalActorFocusStack[i - 1].actionMode;
+    /**
+     * popModal:
+     * @param {Clutter.Actor} actor: the actor passed to original invocation
+     *     of pushModal()
+     * @param {number=} timestamp: optional timestamp
+     *
+     * Reverse the effect of pushModal(). If this invocation is undoing
+     * the topmost invocation, then the focus will be restored to the
+     * previous focus at the time when pushModal() was invoked.
+     *
+     * @timestamp is optionally used to associate the call with a specific user
+     * initiated event. If not provided then the value of
+     * global.get_current_time() is assumed.
+     */
+    popModal(actor, timestamp) {
+        if (timestamp == undefined)
+            timestamp = global.get_current_time();
+
+        let focusIndex = this._findModal(actor);
+        if (focusIndex < 0) {
+            global.stage.set_key_focus(null);
+            global.end_modal(timestamp);
+            this.actionMode = Shell.ActionMode.NORMAL;
+
+            throw new Error('incorrect pop');
         }
-    }
-    modalActorFocusStack.splice(focusIndex, 1);
 
-    if (modalCount > 0)
-        return;
+        this.modalCount -= 1;
+
+        let record = this.modalActorFocusStack[focusIndex];
+        record.actor.disconnect(record.destroyId);
+
+        const { modalActorFocusStack } = this;
+
+        if (focusIndex == this.modalActorFocusStack.length - 1) {
+            if (record.prevFocus)
+                record.prevFocus.disconnect(record.prevFocusDestroyId);
+            this.actionMode = record.actionMode;
+            global.stage.set_key_focus(record.prevFocus);
+        } else {
+            // If we have:
+            //     global.stage.set_focus(a);
+            //     Main.pushModal(b);
+            //     Main.pushModal(c);
+            //     Main.pushModal(d);
+            //
+            // then we have the stack:
+            //     [{ prevFocus: a, actor: b },
+            //      { prevFocus: b, actor: c },
+            //      { prevFocus: c, actor: d }]
+            //
+            // When actor c is destroyed/popped, if we only simply remove the
+            // record, then the focus stack will be [a, c], rather than the correct
+            // [a, b]. Shift the focus stack up before removing the record to ensure
+            // that we get the correct result.
+
+            let t = modalActorFocusStack[modalActorFocusStack.length - 1];
+            if (t.prevFocus)
+                t.prevFocus.disconnect(t.prevFocusDestroyId);
+            // Remove from the middle, shift the focus chain up
+            for (let i = modalActorFocusStack.length - 1; i > focusIndex; i--) {
+                modalActorFocusStack[i].prevFocus = modalActorFocusStack[i - 1].prevFocus;
+                modalActorFocusStack[i].prevFocusDestroyId = modalActorFocusStack[i - 1].prevFocusDestroyId;
+                modalActorFocusStack[i].actionMode = modalActorFocusStack[i - 1].actionMode;
+            }
+        }
+        modalActorFocusStack.splice(focusIndex, 1);
 
-    layoutManager.modalEnded();
-    global.end_modal(timestamp);
-    Meta.enable_unredirect_for_display(global.display);
-    actionMode = Shell.ActionMode.NORMAL;
-}
+        if (this.modalCount > 0)
+            return;
 
-function createLookingGlass() {
-    if (lookingGlass == null)
-        lookingGlass = new LookingGlass.LookingGlass();
+        this.layoutManager.modalEnded();
+        global.end_modal(timestamp);
+        Meta.enable_unredirect_for_display(global.display);
+        this.actionMode = Shell.ActionMode.NORMAL;
+    }
 
-    return lookingGlass;
-}
+    createLookingGlass() {
+        if (this.lookingGlass == null)
+            this.lookingGlass = new LookingGlass.LookingGlass();
 
-function openRunDialog() {
-    if (runDialog == null)
-        runDialog = new RunDialog.RunDialog();
+        return this.lookingGlass;
+    }
 
-    runDialog.open();
-}
+    openRunDialog() {
+        if (this.runDialog == null)
+            this.runDialog = new RunDialog.RunDialog();
 
-function openWelcomeDialog() {
-    if (welcomeDialog === null)
-        welcomeDialog = new WelcomeDialog.WelcomeDialog();
+        this.runDialog.open();
+    }
 
-    welcomeDialog.open();
-}
+    /**
+     * activateWindow:
+     * @param {Meta.Window} window: the window to activate
+     * @param {number=} time: current event time
+     * @param {number=} workspaceNum:  window's workspace number
+     *
+     * Activates @window, switching to its workspace first if necessary,
+     * and switching out of the overview if it's currently active
+     */
+    activateWindow(window, time, workspaceNum) {
+        let workspaceManager = global.workspace_manager;
+        let activeWorkspaceNum = workspaceManager.get_active_workspace_index();
+        let windowWorkspaceNum = workspaceNum !== undefined ? workspaceNum : window.get_workspace().index();
+
+        if (!time)
+            time = global.get_current_time();
+
+        if (windowWorkspaceNum != activeWorkspaceNum) {
+            let workspace = workspaceManager.get_workspace_by_index(windowWorkspaceNum);
+            workspace.activate_with_focus(window, time);
+        } else {
+            window.activate(time);
+        }
 
-/**
- * activateWindow:
- * @param {Meta.Window} window: the window to activate
- * @param {number=} time: current event time
- * @param {number=} workspaceNum:  window's workspace number
- *
- * Activates @window, switching to its workspace first if necessary,
- * and switching out of the overview if it's currently active
- */
-function activateWindow(window, time, workspaceNum) {
-    let workspaceManager = global.workspace_manager;
-    let activeWorkspaceNum = workspaceManager.get_active_workspace_index();
-    let windowWorkspaceNum = workspaceNum !== undefined ? workspaceNum : window.get_workspace().index();
-
-    if (!time)
-        time = global.get_current_time();
-
-    if (windowWorkspaceNum != activeWorkspaceNum) {
-        let workspace = workspaceManager.get_workspace_by_index(windowWorkspaceNum);
-        workspace.activate_with_focus(window, time);
-    } else {
-        window.activate(time);
+        this.overview.hide();
+        this.panel.closeCalendar();
     }
 
-    overview.hide();
-    panel.closeCalendar();
-}
+    _deferredWorkData = {};
 
-// TODO - replace this timeout with some system to guess when the user might
-// be e.g. just reading the screen and not likely to interact.
-var DEFERRED_TIMEOUT_SECONDS = 20;
-var _deferredWorkData = {};
-// Work scheduled for some point in the future
-var _deferredWorkQueue = [];
-// Work we need to process before the next redraw
-var _beforeRedrawQueue = [];
-// Counter to assign work ids
-var _deferredWorkSequence = 0;
-var _deferredTimeoutId = 0;
-
-function _runDeferredWork(workId) {
-    if (!_deferredWorkData[workId])
-        return;
-    let index = _deferredWorkQueue.indexOf(workId);
-    if (index < 0)
-        return;
-
-    _deferredWorkQueue.splice(index, 1);
-    _deferredWorkData[workId].callback();
-    if (_deferredWorkQueue.length == 0 && _deferredTimeoutId > 0) {
-        GLib.source_remove(_deferredTimeoutId);
-        _deferredTimeoutId = 0;
+    // Work scheduled for some point in the future
+    _deferredWorkQueue = [];
+    // Work we need to process before the next redraw
+    _beforeRedrawQueue = [];
+    // Counter to assign work ids
+    _deferredWorkSequence = 0;
+    _deferredTimeoutId = 0;
+
+    _runDeferredWork(workId) {
+        if (!this._deferredWorkData[workId])
+            return;
+        let index = this._deferredWorkQueue.indexOf(workId);
+        if (index < 0)
+            return;
+
+        this._deferredWorkQueue.splice(index, 1);
+        this._deferredWorkData[workId].callback();
+        if (this._deferredWorkQueue.length == 0 && this._deferredTimeoutId > 0) {
+            GLib.source_remove(this._deferredTimeoutId);
+            this._deferredTimeoutId = 0;
+        }
     }
-}
 
-function _runAllDeferredWork() {
-    while (_deferredWorkQueue.length > 0)
-        _runDeferredWork(_deferredWorkQueue[0]);
-}
+    _runAllDeferredWork() {
+        while (this._deferredWorkQueue.length > 0)
+            this._runDeferredWork(this._deferredWorkQueue[0]);
+    }
 
-function _runBeforeRedrawQueue() {
-    for (let i = 0; i < _beforeRedrawQueue.length; i++) {
-        let workId = _beforeRedrawQueue[i];
-        _runDeferredWork(workId);
+    _runBeforeRedrawQueue() {
+        for (let i = 0; i < this._beforeRedrawQueue.length; i++) {
+            let workId = this._beforeRedrawQueue[i];
+            this._runDeferredWork(workId);
+        }
+        this._beforeRedrawQueue = [];
+    }
+
+    _queueBeforeRedraw(workId) {
+        this._beforeRedrawQueue.push(workId);
+        if (this._beforeRedrawQueue.length == 1) {
+            Meta.later_add(Meta.LaterType.BEFORE_REDRAW, () => {
+                this._runBeforeRedrawQueue();
+                return false;
+            });
+        }
     }
-    _beforeRedrawQueue = [];
-}
 
-function _queueBeforeRedraw(workId) {
-    _beforeRedrawQueue.push(workId);
-    if (_beforeRedrawQueue.length == 1) {
-        Meta.later_add(Meta.LaterType.BEFORE_REDRAW, () => {
-            _runBeforeRedrawQueue();
-            return false;
+    /**
+     * initializeDeferredWork:
+     * @param {Clutter.Actor} actor: an actor
+     * @param {(...args: any[]) => any} callback: Function to invoke to perform work
+     *
+     * This function sets up a callback to be invoked when either the
+     * given actor is mapped, or after some period of time when the machine
+     * is idle. This is useful if your actor isn't always visible on the
+     * screen (for example, all actors in the overview), and you don't want
+     * to consume resources updating if the actor isn't actually going to be
+     * displaying to the user.
+     *
+     * Note that queueDeferredWork is called by default immediately on
+     * initialization as well, under the assumption that new actors
+     * will need it.
+     *
+     * @returns {string}: A string work identifier
+     */
+    initializeDeferredWork(actor, callback) {
+        // Turn into a string so we can use as an object property
+        let workId = (++this._deferredWorkSequence).toString();
+        this._deferredWorkData[workId] = {
+            actor,
+            callback
+        };
+        actor.connect('notify::mapped', () => {
+            if (!(actor.mapped && this._deferredWorkQueue.includes(workId)))
+                return;
+            this._queueBeforeRedraw(workId);
         });
+        actor.connect('destroy', () => {
+            let index = this._deferredWorkQueue.indexOf(workId);
+            if (index >= 0)
+                this._deferredWorkQueue.splice(index, 1);
+            delete this._deferredWorkData[workId];
+        });
+        this.queueDeferredWork(workId);
+        return workId;
     }
-}
 
-/**
- * initializeDeferredWork:
- * @param {Clutter.Actor} actor: an actor
- * @param {callback} callback: Function to invoke to perform work
- *
- * This function sets up a callback to be invoked when either the
- * given actor is mapped, or after some period of time when the machine
- * is idle. This is useful if your actor isn't always visible on the
- * screen (for example, all actors in the overview), and you don't want
- * to consume resources updating if the actor isn't actually going to be
- * displaying to the user.
- *
- * Note that queueDeferredWork is called by default immediately on
- * initialization as well, under the assumption that new actors
- * will need it.
- *
- * @returns {string}: A string work identifier
- */
-function initializeDeferredWork(actor, callback) {
-    // Turn into a string so we can use as an object property
-    let workId = (++_deferredWorkSequence).toString();
-    _deferredWorkData[workId] = { actor,
-                                  callback };
-    actor.connect('notify::mapped', () => {
-        if (!(actor.mapped && _deferredWorkQueue.includes(workId)))
+    /**
+     * queueDeferredWork:
+     * @param {string} workId: work identifier
+     *
+     * Ensure that the work identified by @workId will be
+     * run on map or timeout. You should call this function
+     * for example when data being displayed by the actor has
+     * changed.
+     */
+    queueDeferredWork(workId) {
+        let data = this._deferredWorkData[workId];
+        if (!data) {
+            let message = 'Invalid work id %s'.format(workId);
+            logError(new Error(message), message);
             return;
-        _queueBeforeRedraw(workId);
-    });
-    actor.connect('destroy', () => {
-        let index = _deferredWorkQueue.indexOf(workId);
-        if (index >= 0)
-            _deferredWorkQueue.splice(index, 1);
-        delete _deferredWorkData[workId];
-    });
-    queueDeferredWork(workId);
-    return workId;
-}
-
-/**
- * queueDeferredWork:
- * @param {string} workId: work identifier
- *
- * Ensure that the work identified by @workId will be
- * run on map or timeout. You should call this function
- * for example when data being displayed by the actor has
- * changed.
- */
-function queueDeferredWork(workId) {
-    let data = _deferredWorkData[workId];
-    if (!data) {
-        let message = 'Invalid work id %d'.format(workId);
-        logError(new Error(message), message);
-        return;
+        }
+        if (!this._deferredWorkQueue.includes(workId))
+            this._deferredWorkQueue.push(workId);
+        if (data.actor.mapped) {
+            this._queueBeforeRedraw(workId);
+        } else if (this._deferredTimeoutId == 0) {
+            this._deferredTimeoutId = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 
DEFERRED_TIMEOUT_SECONDS, () => {
+                this._runAllDeferredWork();
+                this._deferredTimeoutId = 0;
+                return GLib.SOURCE_REMOVE;
+            });
+            GLib.Source.set_name_by_id(this._deferredTimeoutId, '[gnome-shell] _runAllDeferredWork');
+        }
     }
-    if (!_deferredWorkQueue.includes(workId))
-        _deferredWorkQueue.push(workId);
-    if (data.actor.mapped) {
-        _queueBeforeRedraw(workId);
-    } else if (_deferredTimeoutId == 0) {
-        _deferredTimeoutId = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, DEFERRED_TIMEOUT_SECONDS, () => 
{
-            _runAllDeferredWork();
-            _deferredTimeoutId = 0;
-            return GLib.SOURCE_REMOVE;
-        });
-        GLib.Source.set_name_by_id(_deferredTimeoutId, '[gnome-shell] _runAllDeferredWork');
+    
+    openWelcomeDialog() {
+        if (this.welcomeDialog === null)
+            this.welcomeDialog = new WelcomeDialog.WelcomeDialog();
+    
+        this.welcomeDialog.open();
     }
+};
+    
+export const main = new Main();
+    
+ExtensionUtils._setMain(main);
+
+globalThis.getMain = function getMain() {
+    return main;
 }
 
-var RestartMessage = GObject.registerClass(
-class RestartMessage extends ModalDialog.ModalDialog {
-    _init(message) {
-        super._init({ shellReactive: true,
-                      styleClass: 'restart-message headline',
-                      shouldFadeIn: false,
-                      destroyOnClose: true });
-
-        let label = new St.Label({
-            text: message,
-            x_align: Clutter.ActorAlign.CENTER,
-            y_align: Clutter.ActorAlign.CENTER,
-        });
+export default main;
 
-        this.contentLayout.add_child(label);
-        this.buttonLayout.hide();
-    }
-});
+Gio._promisify(Gio._LocalFilePrototype, 'delete_async', 'delete_finish');
+Gio._promisify(Gio._LocalFilePrototype, 'touch_async', 'touch_finish');
+
+// TODO - replace this timeout with some system to guess when the user might
+// be e.g. just reading the screen and not likely to interact.
+export const DEFERRED_TIMEOUT_SECONDS = 20;
+
+export const RestartMessage = GObject.registerClass(
+    class RestartMessage extends ModalDialog.ModalDialog {
+        _init(message) {
+            super._init({
+                shellReactive: true,
+                styleClass: 'restart-message headline',
+                shouldFadeIn: false,
+                destroyOnClose: true
+            });
+
+            let label = new St.Label({
+                text: message,
+                x_align: Clutter.ActorAlign.CENTER,
+                y_align: Clutter.ActorAlign.CENTER,
+            });
+
+            this.contentLayout.add_child(label);
+            this.buttonLayout.hide();
+        }
+    });
 
 function showRestartMessage(message) {
     let restartMessage = new RestartMessage(message);
     restartMessage.open();
 }
 
-var AnimationsSettings = class {
+export class AnimationsSettings {
     constructor() {
         let backend = global.backend;
         if (!backend.is_rendering_hardware_accelerated()) {
diff --git a/js/ui/messageList.js b/js/ui/messageList.js
index c68ab77fd2..879fa3716f 100644
--- a/js/ui/messageList.js
+++ b/js/ui/messageList.js
@@ -1,14 +1,22 @@
 /* exported MessageListSection */
-const { Atk, Clutter, Gio, GLib,
-        GObject, Graphene, Meta, Pango, St } = imports.gi;
-const Main = imports.ui.main;
-const MessageTray = imports.ui.messageTray;
+import Atk from 'gi://Atk';
+import Clutter from 'gi://Clutter';
+import Gio from 'gi://Gio';
+import GLib from 'gi://GLib';
+import GObject from 'gi://GObject';
+import Graphene from 'gi://Graphene';
+import Meta from 'gi://Meta';
+import Pango from 'gi://Pango';
+import St from 'gi://St';
 
-const Util = imports.misc.util;
+import Main from './main.js';
+import * as MessageTray from './messageTray.js';
 
-var MESSAGE_ANIMATION_TIME = 100;
+import * as Util from '../misc/util.js';
 
-var DEFAULT_EXPAND_LINES = 6;
+export let MESSAGE_ANIMATION_TIME = 100;
+
+export let  DEFAULT_EXPAND_LINES = 6;
 
 function _fixMarkup(text, allowMarkup) {
     if (allowMarkup) {
@@ -31,7 +39,7 @@ function _fixMarkup(text, allowMarkup) {
     return GLib.markup_escape_text(text, -1);
 }
 
-var URLHighlighter = GObject.registerClass(
+export const URLHighlighter = GObject.registerClass(
 class URLHighlighter extends St.Label {
     _init(text = '', lineWrap, allowMarkup) {
         super._init({
@@ -159,7 +167,7 @@ class URLHighlighter extends St.Label {
     }
 });
 
-var ScaleLayout = GObject.registerClass(
+export const ScaleLayout = GObject.registerClass(
 class ScaleLayout extends Clutter.BinLayout {
     _init(params) {
         this._container = null;
@@ -188,6 +196,9 @@ class ScaleLayout extends Clutter.BinLayout {
         }
     }
 
+    /**
+     * @returns {[number, number]}
+     */
     vfunc_get_preferred_width(container, forHeight) {
         this._connectContainer(container);
 
@@ -196,6 +207,9 @@ class ScaleLayout extends Clutter.BinLayout {
                 Math.floor(nat * container.scale_x)];
     }
 
+    /**
+     * @returns {[number, number]}
+     */
     vfunc_get_preferred_height(container, forWidth) {
         this._connectContainer(container);
 
@@ -205,7 +219,7 @@ class ScaleLayout extends Clutter.BinLayout {
     }
 });
 
-var LabelExpanderLayout = GObject.registerClass({
+export const LabelExpanderLayout = GObject.registerClass({
     Properties: {
         'expansion': GObject.ParamSpec.double('expansion',
                                               'Expansion',
@@ -215,6 +229,9 @@ var LabelExpanderLayout = GObject.registerClass({
                                               0, 1, 0),
     },
 }, class LabelExpanderLayout extends Clutter.LayoutManager {
+    /**
+     * @param {*} params 
+     */
     _init(params) {
         this._expansion = 0;
         this._expandLines = DEFAULT_EXPAND_LINES;
@@ -251,6 +268,9 @@ var LabelExpanderLayout = GObject.registerClass({
         this._container = container;
     }
 
+    /**
+     * @returns {[number, number]}
+     */
     vfunc_get_preferred_width(container, forHeight) {
         let [min, nat] = [0, 0];
 
@@ -266,6 +286,9 @@ var LabelExpanderLayout = GObject.registerClass({
         return [min, nat];
     }
 
+    /**
+     * @returns {[number, number]}
+     */
     vfunc_get_preferred_height(container, forWidth) {
         let [min, nat] = [0, 0];
 
@@ -295,13 +318,17 @@ var LabelExpanderLayout = GObject.registerClass({
 });
 
 
-var Message = GObject.registerClass({
+export const Message = GObject.registerClass({
     Signals: {
         'close': {},
         'expanded': {},
         'unexpanded': {},
     },
 }, class Message extends St.Button {
+    /**
+     * @param {*} title 
+     * @param {*} body 
+     */
     _init(title, body) {
         super._init({
             style_class: 'message',
@@ -313,6 +340,7 @@ var Message = GObject.registerClass({
 
         this.expanded = false;
         this._useBodyMarkup = false;
+        this.notification = null;
 
         let vbox = new St.BoxLayout({
             vertical: true,
@@ -361,11 +389,11 @@ var Message = GObject.registerClass({
         });
         titleBox.add_actor(this._closeButton);
 
-        this._bodyStack = new St.Widget({ x_expand: true });
-        this._bodyStack.layout_manager = new LabelExpanderLayout();
+        this._bodyStack = new St.Widget({ x_expand: true, layout_manager: new LabelExpanderLayout() });
+       
         contentBox.add_actor(this._bodyStack);
 
-        this.bodyLabel = new URLHighlighter('', false, this._useBodyMarkup);
+        this.bodyLabel = new URLHighlighter({ text: '', lineWrap: false, allowMarkup: this._useBodyMarkup });
         this.bodyLabel.add_style_class_name('message-body');
         this._bodyStack.add_actor(this.bodyLabel);
         this.setBody(body);
@@ -532,7 +560,7 @@ var Message = GObject.registerClass({
     }
 });
 
-var MessageListSection = GObject.registerClass({
+export const MessageListSection = GObject.registerClass({
     Properties: {
         'can-clear': GObject.ParamSpec.boolean(
             'can-clear', 'can-clear', 'can-clear',
@@ -557,6 +585,7 @@ var MessageListSection = GObject.registerClass({
             x_expand: true,
         });
 
+        /** @type {St.BoxLayout<St.Bin<Message["prototype"]>>} */
         this._list = new St.BoxLayout({ style_class: 'message-list-section-list',
                                         vertical: true });
         this.add_actor(this._list);
diff --git a/js/ui/messageTray.js b/js/ui/messageTray.js
index 7596f105f6..fb32e52cad 100644
--- a/js/ui/messageTray.js
+++ b/js/ui/messageTray.js
@@ -3,32 +3,38 @@
    NotificationApplicationPolicy, Source, SourceActor,
    SystemNotificationSource, MessageTray */
 
-const { Clutter, Gio, GLib, GObject, Meta, Shell, St } = imports.gi;
-
-const Calendar = imports.ui.calendar;
-const GnomeSession = imports.misc.gnomeSession;
-const Layout = imports.ui.layout;
-const Main = imports.ui.main;
+import Clutter from 'gi://Clutter';
+import Gio from 'gi://Gio';
+import GLib from 'gi://GLib';
+import GObject from 'gi://GObject';
+import Meta from 'gi://Meta';
+import Shell from 'gi://Shell';
+import St from 'gi://St';
+
+import * as Calendar from './calendar.js';
+import * as GnomeSession from '../misc/gnomeSession.js';
+import * as Layout from './layout.js';
+import Main from './main.js';
 
 const SHELL_KEYBINDINGS_SCHEMA = 'org.gnome.shell.keybindings';
 
-var ANIMATION_TIME = 200;
-var NOTIFICATION_TIMEOUT = 4000;
+export let  ANIMATION_TIME = 200;
+export let  NOTIFICATION_TIMEOUT = 4000;
 
-var HIDE_TIMEOUT = 200;
-var LONGER_HIDE_TIMEOUT = 600;
+export let  HIDE_TIMEOUT = 200;
+export let  LONGER_HIDE_TIMEOUT = 600;
 
-var MAX_NOTIFICATIONS_IN_QUEUE = 3;
-var MAX_NOTIFICATIONS_PER_SOURCE = 3;
-var MAX_NOTIFICATION_BUTTONS = 3;
+export let  MAX_NOTIFICATIONS_IN_QUEUE = 3;
+export let  MAX_NOTIFICATIONS_PER_SOURCE = 3;
+export let  MAX_NOTIFICATION_BUTTONS = 3;
 
 // We delay hiding of the tray if the mouse is within MOUSE_LEFT_ACTOR_THRESHOLD
 // range from the point where it left the tray.
-var MOUSE_LEFT_ACTOR_THRESHOLD = 20;
+export let  MOUSE_LEFT_ACTOR_THRESHOLD = 20;
 
-var IDLE_TIME = 1000;
+export let  IDLE_TIME = 1000;
 
-var State = {
+export const State = {
     HIDDEN:  0,
     SHOWING: 1,
     SHOWN:   2,
@@ -42,7 +48,8 @@ var State = {
 // notifications that were requested to be destroyed by the associated source,
 // and REPLACED for notifications that were destroyed as a consequence of a
 // newer version having replaced them.
-var NotificationDestroyedReason = {
+/** @enum {number} */
+export const NotificationDestroyedReason = {
     EXPIRED: 1,
     DISMISSED: 2,
     SOURCE_CLOSED: 3,
@@ -53,7 +60,8 @@ var NotificationDestroyedReason = {
 // urgency values map to the corresponding values for the notifications received
 // through the notification daemon. HIGH urgency value is used for chats received
 // through the Telepathy client.
-var Urgency = {
+/** @enum {number} */
+export const Urgency = {
     LOW: 0,
     NORMAL: 1,
     HIGH: 2,
@@ -66,12 +74,13 @@ var Urgency = {
 // contain information private to the physical system (for example, battery
 // status) and hence the same for every user. This affects whether the content
 // of a notification is shown on the lock screen.
-var PrivacyScope = {
+/** @enum {number} */
+export const PrivacyScope = {
     USER: 0,
     SYSTEM: 1,
 };
 
-var FocusGrabber = class FocusGrabber {
+export class FocusGrabber {
     constructor(actor) {
         this._actor = actor;
         this._prevKeyFocusActor = null;
@@ -132,7 +141,7 @@ var FocusGrabber = class FocusGrabber {
 // source, such as whether to play sound or honour the critical bit.
 //
 // A notification without a policy object will inherit the default one.
-var NotificationPolicy = GObject.registerClass({
+export const NotificationPolicy = GObject.registerClass({
     GTypeFlags: GObject.TypeFlags.ABSTRACT,
     Properties: {
         'enable': GObject.ParamSpec.boolean(
@@ -187,7 +196,7 @@ var NotificationPolicy = GObject.registerClass({
     }
 });
 
-var NotificationGenericPolicy = GObject.registerClass({
+export const NotificationGenericPolicy = GObject.registerClass({
 }, class NotificationGenericPolicy extends NotificationPolicy {
     _init() {
         super._init();
@@ -204,6 +213,7 @@ var NotificationGenericPolicy = GObject.registerClass({
     }
 
     _changed(settings, key) {
+        // @ts-expect-error this.constructor cannot be statically analyzed.
         if (this.constructor.find_property(key))
             this.notify(key);
     }
@@ -217,7 +227,7 @@ var NotificationGenericPolicy = GObject.registerClass({
     }
 });
 
-var NotificationApplicationPolicy = GObject.registerClass({
+export const NotificationApplicationPolicy = GObject.registerClass({
 }, class NotificationApplicationPolicy extends NotificationPolicy {
     _init(id) {
         super._init();
@@ -253,6 +263,8 @@ var NotificationApplicationPolicy = GObject.registerClass({
     }
 
     _changed(settings, key) {
+        // FIXME
+        // @ts-expect-error
         if (this.constructor.find_property(key))
             this.notify(key);
     }
@@ -345,7 +357,7 @@ var NotificationApplicationPolicy = GObject.registerClass({
 // @source allows playing sounds).
 //
 // [1] https://developer.gnome.org/notification-spec/#markup
-var Notification = GObject.registerClass({
+export const Notification = GObject.registerClass({
     Properties: {
         'acknowledged': GObject.ParamSpec.boolean(
             'acknowledged', 'acknowledged', 'acknowledged',
@@ -358,6 +370,12 @@ var Notification = GObject.registerClass({
         'updated': { param_types: [GObject.TYPE_BOOLEAN] },
     },
 }, class Notification extends GObject.Object {
+    /**
+     * @param {*} source 
+     * @param {*} title 
+     * @param {*} banner 
+     * @param {*} params 
+     */
     _init(source, title, banner, params) {
         super._init();
 
@@ -376,6 +394,8 @@ var Notification = GObject.registerClass({
         this.actions = [];
         this.setResident(false);
 
+        this.answered = false;
+
         // If called with only one argument we assume the caller
         // will call .update() later on. This is the case of
         // NotificationDaemon, which wants to use the same code
@@ -507,7 +527,7 @@ var Notification = GObject.registerClass({
     }
 });
 
-var NotificationBanner = GObject.registerClass({
+export const NotificationBanner = GObject.registerClass({
     Signals: {
         'done-displaying': {},
         'unfocused': {},
@@ -606,7 +626,7 @@ var NotificationBanner = GObject.registerClass({
     }
 });
 
-var SourceActor = GObject.registerClass(
+export const SourceActor = GObject.registerClass(
 class SourceActor extends St.Widget {
     _init(source, size) {
         super._init();
@@ -645,7 +665,13 @@ class SourceActor extends St.Widget {
     }
 });
 
-var Source = GObject.registerClass({
+/** 
+ * @typedef {object} SourceParams
+ * @property {string} [title]
+ * @property {string} [iconName]
+ */
+
+export const Source = GObject.registerClass({
     Properties: {
         'count': GObject.ParamSpec.int(
             'count', 'count', 'count',
@@ -707,6 +733,9 @@ var Source = GObject.registerClass({
         this.notify('count');
     }
 
+    /**
+     * @returns {NotificationPolicy["prototype"]}
+     */
     _createPolicy() {
         return new NotificationGenericPolicy();
     }
@@ -737,6 +766,9 @@ var Source = GObject.registerClass({
                              icon_size: size });
     }
 
+    /**
+     * @returns {Gio.Icon}
+     */
     getIcon() {
         return new Gio.ThemedIcon({ name: this.iconName });
     }
@@ -808,7 +840,7 @@ var Source = GObject.registerClass({
     }
 });
 
-var MessageTray = GObject.registerClass({
+export const MessageTray = GObject.registerClass({
     Signals: {
         'queue-changed': {},
         'source-added': { param_types: [Source.$gtype] },
@@ -822,7 +854,7 @@ var MessageTray = GObject.registerClass({
             layout_manager: new Clutter.BinLayout(),
         });
 
-        this._presence = new GnomeSession.Presence((proxy, _error) => {
+        this._presence = GnomeSession.Presence((proxy, _error) => {
             this._onStatusChanged(proxy.status);
         });
         this._busy = false;
@@ -1438,10 +1470,10 @@ var MessageTray = GObject.registerClass({
     }
 });
 
-var SystemNotificationSource = GObject.registerClass(
+export const SystemNotificationSource = GObject.registerClass(
 class SystemNotificationSource extends Source {
     _init() {
-        super._init(_("System Information"), 'dialog-information-symbolic');
+        super._init({ title: _("System Information"), iconName: 'dialog-information-symbolic' });
     }
 
     open() {
diff --git a/js/ui/modalDialog.js b/js/ui/modalDialog.js
index 10fe90310c..74b5383736 100644
--- a/js/ui/modalDialog.js
+++ b/js/ui/modalDialog.js
@@ -1,17 +1,22 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported ModalDialog */
 
-const { Atk, Clutter, GObject, Shell, St } = imports.gi;
-
-const Dialog = imports.ui.dialog;
-const Layout = imports.ui.layout;
-const Lightbox = imports.ui.lightbox;
-const Main = imports.ui.main;
-
-var OPEN_AND_CLOSE_TIME = 100;
-var FADE_OUT_DIALOG_TIME = 1000;
-
-var State = {
+import Atk from 'gi://Atk';
+import Clutter from 'gi://Clutter';
+import GObject from 'gi://GObject';
+import Shell from 'gi://Shell';
+import St from 'gi://St';
+
+import * as Dialog from './dialog.js';
+import * as Layout from './layout.js';
+import * as Lightbox from './lightbox.js';
+import Main from './main.js';
+
+export let OPEN_AND_CLOSE_TIME = 100;
+export let FADE_OUT_DIALOG_TIME = 1000;
+
+/** @enum {number} */
+export const State = {
     OPENED: 0,
     CLOSED: 1,
     OPENING: 2,
@@ -19,7 +24,17 @@ var State = {
     FADED_OUT: 4,
 };
 
-var ModalDialog = GObject.registerClass({
+/** 
+ * @typedef {object} ModalDialogParams
+ * @property {boolean} shellReactive
+ * @property {string | null} styleClass
+ * @property {Shell.ActionMode} actionMode
+ * @property {boolean} shouldFadeIn
+ * @property {boolean} shouldFadeOut
+ * @property {boolean} destroyOnClose
+ */
+
+export const ModalDialog = GObject.registerClass({
     Properties: {
         'state': GObject.ParamSpec.int('state', 'Dialog state', 'state',
                                        GObject.ParamFlags.READABLE,
@@ -29,7 +44,7 @@ var ModalDialog = GObject.registerClass({
     },
     Signals: { 'opened': {}, 'closed': {} },
 }, class ModalDialog extends St.Widget {
-    _init(params = {}) {
+    _init(params = {}, ...args) {
         super._init({ visible: false,
                       x: 0,
                       y: 0,
@@ -258,7 +273,7 @@ var ModalDialog = GObject.registerClass({
             opacity: 0,
             duration: FADE_OUT_DIALOG_TIME,
             mode: Clutter.AnimationMode.EASE_OUT_QUAD,
-            onComplete: () => (this.state = State.FADED_OUT),
+            onComplete: () => (this._setState(State.FADED_OUT)),
         });
     }
 });
diff --git a/js/ui/mpris.js b/js/ui/mpris.js
index 6038142c6d..98e3b8c0a5 100644
--- a/js/ui/mpris.js
+++ b/js/ui/mpris.js
@@ -1,11 +1,14 @@
 /* exported MediaSection */
-const { Gio, GObject, Shell, St } = imports.gi;
-const Signals = imports.misc.signals;
+import Gio from 'gi://Gio';
+import GObject from 'gi://GObject';
+import Shell from 'gi://Shell';
+import St from 'gi://St';
+import * as Signals from '../misc/signals.js';
 
-const Main = imports.ui.main;
-const MessageList = imports.ui.messageList;
+import Main from './main.js';
+import * as MessageList from './messageList.js';
 
-const { loadInterfaceXML } = imports.misc.fileUtils;
+import { loadInterfaceXML } from '../misc/fileUtilsModule.js';
 
 const DBusIface = loadInterfaceXML('org.freedesktop.DBus');
 const DBusProxy = Gio.DBusProxy.makeProxyWrapper(DBusIface);
@@ -18,8 +21,11 @@ const MprisPlayerProxy = Gio.DBusProxy.makeProxyWrapper(MprisPlayerIface);
 
 const MPRIS_PLAYER_PREFIX = 'org.mpris.MediaPlayer2.';
 
-var MediaMessage = GObject.registerClass(
+export const MediaMessage = GObject.registerClass(
 class MediaMessage extends MessageList.Message {
+    /**
+     * @param {*} player 
+     */
     _init(player) {
         super._init('', '');
 
@@ -93,7 +99,7 @@ class MediaMessage extends MessageList.Message {
     }
 });
 
-var MprisPlayer = class MprisPlayer extends Signals.EventEmitter {
+export class MprisPlayer extends Signals.EventEmitter {
     constructor(busName) {
         super();
 
@@ -243,7 +249,7 @@ var MprisPlayer = class MprisPlayer extends Signals.EventEmitter {
     }
 };
 
-var MediaSection = GObject.registerClass(
+export const MediaSection = GObject.registerClass(
 class MediaSection extends MessageList.MessageListSection {
     _init() {
         super._init();
diff --git a/js/ui/notificationDaemon.js b/js/ui/notificationDaemon.js
index e75c2fdc0e..e4de618daf 100644
--- a/js/ui/notificationDaemon.js
+++ b/js/ui/notificationDaemon.js
@@ -1,30 +1,37 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported NotificationDaemon */
 
-const { GdkPixbuf, Gio, GLib, GObject, Shell, St } = imports.gi;
+import GdkPixbuf from 'gi://GdkPixbuf';
+import Gio from 'gi://Gio';
+import GLib from 'gi://GLib';
+import GObject from 'gi://GObject';
+import Shell from 'gi://Shell';
+import St from 'gi://St';
 
 const Config = imports.misc.config;
-const Main = imports.ui.main;
-const MessageTray = imports.ui.messageTray;
+import Main from './main.js';
+import * as MessageTray from './messageTray.js';
 
-const { loadInterfaceXML } = imports.misc.fileUtils;
+import { loadInterfaceXML } from '../misc/fileUtilsModule.js';
 
 const FdoNotificationsIface = loadInterfaceXML('org.freedesktop.Notifications');
 
-var NotificationClosedReason = {
+/** @enum {number} */
+export const NotificationClosedReason = {
     EXPIRED: 1,
     DISMISSED: 2,
     APP_CLOSED: 3,
     UNDEFINED: 4,
 };
 
-var Urgency = {
+/** @enum {number} */
+export const Urgency = {
     LOW: 0,
     NORMAL: 1,
     CRITICAL: 2,
 };
 
-var FdoNotificationDaemon = class FdoNotificationDaemon {
+export class FdoNotificationDaemon {
     constructor() {
         this._dbusImpl = Gio.DBusExportedObject.wrapJSObject(FdoNotificationsIface, this);
         this._dbusImpl.export(Gio.DBus.session, '/org/freedesktop/Notifications');
@@ -187,7 +194,8 @@ var FdoNotificationDaemon = class FdoNotificationDaemon {
         let sender = invocation.get_sender();
         let pid = hints['sender-pid'];
 
-        let source = this._getSource(appName, pid, ndata, sender, null);
+        // FIXME
+        let source = this._getSource(appName, pid, ndata, sender);
         this._notifyForSource(source, ndata);
 
         return invocation.return_value(GLib.Variant.new('(u)', [id]));
@@ -342,7 +350,15 @@ var FdoNotificationDaemon = class FdoNotificationDaemon {
     }
 };
 
-var FdoNotificationDaemonSource = GObject.registerClass(
+/**
+ * @typedef {object} FdoNotificationDaemonSourceParams
+ * @property {string} title
+ * @property {number} pid
+ * @property {string} sender
+ * @property {string} appId
+ */
+
+export const FdoNotificationDaemonSource = GObject.registerClass(
 class FdoNotificationDaemonSource extends MessageTray.Source {
     _init(title, pid, sender, appId) {
         this.pid = pid;
@@ -466,8 +482,12 @@ const PRIORITY_URGENCY_MAP = {
     urgent: MessageTray.Urgency.CRITICAL,
 };
 
-var GtkNotificationDaemonNotification = GObject.registerClass(
+export const GtkNotificationDaemonNotification = GObject.registerClass(
 class GtkNotificationDaemonNotification extends MessageTray.Notification {
+    /**
+     * @param {*} source 
+     * @param {*} notification 
+     */
     _init(source, notification) {
         super._init(source);
         this._serialized = GLib.Variant.new('a{sv}', notification);
@@ -542,14 +562,19 @@ function objectPathFromAppId(appId) {
     return '/' + appId.replace(/\./g, '/').replace(/-/g, '_');
 }
 
-function getPlatformData() {
-    let startupId = GLib.Variant.new('s', '_TIME%s'.format(global.get_current_time()));
+export function getPlatformData() {
+    let startupId = GLib.Variant.new('s', '_TIME%d'.format(global.get_current_time()));
     return { "desktop-startup-id": startupId };
 }
 
 function InvalidAppError() {}
 
-var GtkNotificationDaemonAppSource = GObject.registerClass(
+/**
+ * @typedef {object} GtkNotificationDaemonAppSourceParams
+ * @property {string} appId
+ */
+
+export const GtkNotificationDaemonAppSource = GObject.registerClass(
 class GtkNotificationDaemonAppSource extends MessageTray.Source {
     _init(appId) {
         let objectPath = objectPathFromAppId(appId);
@@ -651,7 +676,7 @@ class GtkNotificationDaemonAppSource extends MessageTray.Source {
 
 const GtkNotificationsIface = loadInterfaceXML('org.gtk.Notifications');
 
-var GtkNotificationDaemon = class GtkNotificationDaemon {
+export class GtkNotificationDaemon {
     constructor() {
         this._sources = {};
 
@@ -756,7 +781,7 @@ var GtkNotificationDaemon = class GtkNotificationDaemon {
     }
 };
 
-var NotificationDaemon = class NotificationDaemon {
+export class NotificationDaemon {
     constructor() {
         this._fdoNotificationDaemon = new FdoNotificationDaemon();
         this._gtkNotificationDaemon = new GtkNotificationDaemon();
diff --git a/js/ui/osdMonitorLabeler.js b/js/ui/osdMonitorLabeler.js
index b242ecca1c..80099440ca 100644
--- a/js/ui/osdMonitorLabeler.js
+++ b/js/ui/osdMonitorLabeler.js
@@ -1,11 +1,16 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported OsdMonitorLabeler */
 
-const { Clutter, Gio, GObject, Meta, St } = imports.gi;
+import Clutter from 'gi://Clutter';
+import Gio from 'gi://Gio';
+import GObject from 'gi://GObject';
+import Meta from 'gi://Meta';
+import St from 'gi://St';
 
-const Main = imports.ui.main;
 
-var OsdMonitorLabel = GObject.registerClass(
+import Main from './main.js';
+
+export const OsdMonitorLabel = GObject.registerClass(
 class OsdMonitorLabel extends St.Widget {
     _init(monitor, label) {
         super._init({ x_expand: true, y_expand: true });
@@ -42,7 +47,7 @@ class OsdMonitorLabel extends St.Widget {
     }
 });
 
-var OsdMonitorLabeler = class {
+export class OsdMonitorLabeler {
     constructor() {
         this._monitorManager = Meta.MonitorManager.get();
         this._client = null;
diff --git a/js/ui/osdWindow.js b/js/ui/osdWindow.js
index bae4b862ad..8f98c549b3 100644
--- a/js/ui/osdWindow.js
+++ b/js/ui/osdWindow.js
@@ -1,17 +1,21 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported OsdWindowManager */
 
-const { Clutter, GLib, GObject, Meta, St } = imports.gi;
+import Clutter from 'gi://Clutter';
+import GLib from 'gi://GLib';
+import GObject from 'gi://GObject';
+import Meta from 'gi://Meta';
+import St from 'gi://St';
 
-const BarLevel = imports.ui.barLevel;
-const Layout = imports.ui.layout;
-const Main = imports.ui.main;
+import * as BarLevel from './barLevel.js';
+import * as Layout from './layout.js';
+import Main from './main.js';
 
-var HIDE_TIMEOUT = 1500;
-var FADE_TIME = 100;
-var LEVEL_ANIMATION_TIME = 100;
+export let HIDE_TIMEOUT = 1500;
+export let FADE_TIME = 100;
+export let LEVEL_ANIMATION_TIME = 100;
 
-var OsdWindowConstraint = GObject.registerClass(
+export const OsdWindowConstraint = GObject.registerClass(
 class OsdWindowConstraint extends Clutter.Constraint {
     _init(props) {
         this._minSize = 0;
@@ -41,7 +45,7 @@ class OsdWindowConstraint extends Clutter.Constraint {
     }
 });
 
-var OsdWindow = GObject.registerClass(
+export const OsdWindow = GObject.registerClass(
 class OsdWindow extends St.Widget {
     _init(monitorIndex) {
         super._init({
@@ -199,7 +203,7 @@ class OsdWindow extends St.Widget {
     }
 });
 
-var OsdWindowManager = class {
+export class OsdWindowManager {
     constructor() {
         this._osdWindows = [];
         Main.layoutManager.connect('monitors-changed',
diff --git a/js/ui/overview.js b/js/ui/overview.js
index 176931c612..31d8f82e21 100644
--- a/js/ui/overview.js
+++ b/js/ui/overview.js
@@ -1,27 +1,33 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported Overview */
 
-const { Clutter, Gio, GLib, GObject, Meta, Shell, St } = imports.gi;
-const Signals = imports.misc.signals;
+import Clutter from 'gi://Clutter';
+import Gio from 'gi://Gio';
+import GLib from 'gi://GLib';
+import GObject from 'gi://GObject';
+import Meta from 'gi://Meta';
+import Shell from 'gi://Shell';
+import St from 'gi://St';
+import * as Signals from '../misc/signals.js';
 
 // Time for initial animation going into Overview mode;
 // this is defined here to make it available in imports.
-var ANIMATION_TIME = 250;
+export let ANIMATION_TIME = 250;
 
-const DND = imports.ui.dnd;
-const LayoutManager = imports.ui.layout;
-const Main = imports.ui.main;
-const MessageTray = imports.ui.messageTray;
-const OverviewControls = imports.ui.overviewControls;
-const SwipeTracker = imports.ui.swipeTracker;
-const WindowManager = imports.ui.windowManager;
-const WorkspaceThumbnail = imports.ui.workspaceThumbnail;
+import * as DND from './dnd.js';
+import * as LayoutManager from './layout.js';
+import Main from './main.js';
+import * as MessageTray from './messageTray.js';
+import * as OverviewControls from './overviewControls.js';
+import * as SwipeTracker from './swipeTracker.js';
+import * as WindowManager from './windowManager.js';
+import * as WorkspaceThumbnail from './workspaceThumbnail.js';
 
-var DND_WINDOW_SWITCH_TIMEOUT = 750;
+export let DND_WINDOW_SWITCH_TIMEOUT = 750;
 
-var OVERVIEW_ACTIVATION_TIMEOUT = 0.5;
+export let OVERVIEW_ACTIVATION_TIMEOUT = 0.5;
 
-var ShellInfo = class {
+export class ShellInfo {
     constructor() {
         this._source = null;
     }
@@ -57,7 +63,7 @@ var ShellInfo = class {
     }
 };
 
-var OverviewActor = GObject.registerClass(
+export const OverviewActor = GObject.registerClass(
 class OverviewActor extends St.BoxLayout {
     _init() {
         super._init({
@@ -83,6 +89,7 @@ class OverviewActor extends St.BoxLayout {
     }
 
     runStartupAnimation(callback) {
+        log('animating...');
         this._controls.runStartupAnimation(callback);
     }
 
@@ -99,7 +106,7 @@ class OverviewActor extends St.BoxLayout {
     }
 });
 
-var Overview = class extends Signals.EventEmitter {
+export class Overview extends Signals.EventEmitter {
     constructor() {
         super();
 
@@ -641,6 +648,7 @@ var Overview = class extends Signals.EventEmitter {
     }
 
     runStartupAnimation(callback) {
+        log('trackin')
         Main.panel.style = 'transition-duration: 0ms;';
 
         this._shown = true;
@@ -654,6 +662,7 @@ var Overview = class extends Signals.EventEmitter {
         Meta.disable_unredirect_for_display(global.display);
 
         this.emit('showing');
+        log('SHOWING');
 
         this._overview.runStartupAnimation(() => {
             if (!this._syncGrab()) {
diff --git a/js/ui/overviewControls.js b/js/ui/overviewControls.js
index 6cbaa22ba9..356e7a653d 100644
--- a/js/ui/overviewControls.js
+++ b/js/ui/overviewControls.js
@@ -1,33 +1,41 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported ControlsManager */
 
-const { Clutter, Gio, GLib, GObject, Meta, Shell, St } = imports.gi;
-
-const AppDisplay = imports.ui.appDisplay;
-const Dash = imports.ui.dash;
-const Layout = imports.ui.layout;
-const Main = imports.ui.main;
-const Overview = imports.ui.overview;
-const SearchController = imports.ui.searchController;
-const Util = imports.misc.util;
-const WindowManager = imports.ui.windowManager;
-const WorkspaceThumbnail = imports.ui.workspaceThumbnail;
-const WorkspacesView = imports.ui.workspacesView;
+import Clutter from 'gi://Clutter';
+import Gio from 'gi://Gio';
+import GObject from 'gi://GObject';
+import Meta from 'gi://Meta';
+import Shell from 'gi://Shell';
+import St from 'gi://St';
+
+import * as AppDisplay from './appDisplay.js';
+import * as Dash from './dash.js';
+import * as Layout from './layout.js';
+import Main from './main.js';
+import * as Overview from './overview.js';
+import * as SearchController from './searchController.js';
+import * as Util from '../misc/util.js';
+import * as WindowManager from './windowManager.js';
+import * as WorkspaceThumbnail from './workspaceThumbnail.js';
+import * as WorkspacesView from './workspacesView.js';
 
 const SMALL_WORKSPACE_RATIO = 0.15;
 const DASH_MAX_HEIGHT_RATIO = 0.15;
 
 const A11Y_SCHEMA = 'org.gnome.desktop.a11y.keyboard';
 
-var SIDE_CONTROLS_ANIMATION_TIME = Overview.ANIMATION_TIME;
+// TODO
+// import { ANIMATION_TIME as SIDE_CONTROLS_ANIMATION_TIME } from './overview.js';
+// export { ANIMATION_TIME as SIDE_CONTROLS_ANIMATION_TIME } from './overview.js';
+export let SIDE_CONTROLS_ANIMATION_TIME = 250;
 
-var ControlsState = {
+export const ControlsState = {
     HIDDEN: 0,
     WINDOW_PICKER: 1,
     APP_GRID: 2,
 };
 
-var ControlsManagerLayout = GObject.registerClass(
+export const ControlsManagerLayout = GObject.registerClass(
 class ControlsManagerLayout extends Clutter.BoxLayout {
     _init(searchEntry, appDisplay, workspacesDisplay, workspacesThumbnails,
         searchController, dash, stateAdjustment) {
@@ -118,11 +126,13 @@ class ControlsManagerLayout extends Clutter.BoxLayout {
             this.hookup_style(container);
     }
 
+    /** @returns {[number, number]} */
     vfunc_get_preferred_width(_container, _forHeight) {
         // The MonitorConstraint will allocate us a fixed size anyway
         return [0, 0];
     }
 
+    /** @returns {[number, number]} */
     vfunc_get_preferred_height(_container, _forWidth) {
         // The MonitorConstraint will allocate us a fixed size anyway
         return [0, 0];
@@ -241,7 +251,7 @@ class ControlsManagerLayout extends Clutter.BoxLayout {
     }
 });
 
-var OverviewAdjustment = GObject.registerClass({
+export const OverviewAdjustment = GObject.registerClass({
     Properties: {
         'gesture-in-progress': GObject.ParamSpec.boolean(
             'gesture-in-progress', 'Gesture in progress', 'Gesture in progress',
@@ -249,6 +259,9 @@ var OverviewAdjustment = GObject.registerClass({
             false),
     },
 }, class OverviewAdjustment extends St.Adjustment {
+    /** @type {boolean} */
+    gestureInProgress;
+
     _init(actor) {
         super._init({
             actor,
@@ -262,12 +275,14 @@ var OverviewAdjustment = GObject.registerClass({
         const currentState = this.value;
 
         const transition = this.get_transition('value');
-        let initialState = transition
+        /** @type {number} */
+        let initialState = (transition
             ? transition.get_interval().peek_initial_value()
-            : currentState;
-        let finalState = transition
+            : currentState);
+        /** @type {number} */
+        let finalState = (transition
             ? transition.get_interval().peek_final_value()
-            : currentState;
+            : currentState);
 
         if (initialState > finalState) {
             initialState = Math.ceil(initialState);
@@ -292,7 +307,7 @@ var OverviewAdjustment = GObject.registerClass({
     }
 });
 
-var ControlsManager = GObject.registerClass(
+export const ControlsManager = GObject.registerClass(
 class ControlsManager extends St.Widget {
     _init() {
         super._init({
@@ -375,6 +390,7 @@ class ControlsManager extends St.Widget {
         this.add_child(this._thumbnailsBox);
         this.add_child(this._workspacesDisplay);
 
+        /** @type {ControlsManagerLayout["prototype"]} */
         this.layout_manager = new ControlsManagerLayout(
             this._searchEntryBin,
             this._appDisplay,
@@ -718,7 +734,7 @@ class ControlsManager extends St.Widget {
     }
 
     getWorkspacesBoxForState(state) {
-        return this.layoutManager.getWorkspacesBoxForState(state);
+        return this.layout_manager.getWorkspacesBoxForState(state);
     }
 
     gestureBegin(tracker) {
diff --git a/js/ui/padOsd.js b/js/ui/padOsd.js
index 1f3abe14c8..c71b24d59b 100644
--- a/js/ui/padOsd.js
+++ b/js/ui/padOsd.js
@@ -1,15 +1,24 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported PadOsd, PadOsdService */
 
-const { Atk, Clutter, GDesktopEnums, Gio,
-        GLib, GObject, Gtk, Meta, Pango, Rsvg, St } = imports.gi;
-const Signals = imports.misc.signals;
-
-const Main = imports.ui.main;
-const PopupMenu = imports.ui.popupMenu;
-const Layout = imports.ui.layout;
-
-const { loadInterfaceXML } = imports.misc.fileUtils;
+import Atk from 'gi://Atk';
+import Clutter from 'gi://Clutter';
+import GDesktopEnums from 'gi://GDesktopEnums';
+import Gio from 'gi://Gio';
+import GLib from 'gi://GLib';
+import GObject from 'gi://GObject';
+import Gtk from 'gi://Gtk';
+import Meta from 'gi://Meta';
+import Pango from 'gi://Pango';
+import Rsvg from 'gi://Rsvg';
+import St from 'gi://St';
+import * as Signals from '../misc/signals.js';
+
+import Main from './main.js';
+import * as PopupMenu from './popupMenu.js';
+import * as Layout from './layout.js';
+
+import { loadInterfaceXML } from '../misc/fileUtilsModule.js';
 
 const ACTIVE_COLOR = "#729fcf";
 
@@ -22,9 +31,13 @@ const CCW = 1;
 const UP = 0;
 const DOWN = 1;
 
-var PadChooser = GObject.registerClass({
+export const PadChooser = GObject.registerClass({
     Signals: { 'pad-selected': { param_types: [Clutter.InputDevice.$gtype] } },
 }, class PadChooser extends St.Button {
+    /**
+     * @param {*} device 
+     * @param {*} groupDevices 
+     */
     _init(device, groupDevices) {
         super._init({
             style_class: 'pad-chooser-button',
@@ -88,7 +101,7 @@ var PadChooser = GObject.registerClass({
     }
 });
 
-var KeybindingEntry = GObject.registerClass({
+export const KeybindingEntry = GObject.registerClass({
     Signals: { 'keybinding-edited': { param_types: [GObject.TYPE_STRING] } },
 }, class KeybindingEntry extends St.Entry {
     _init() {
@@ -109,7 +122,7 @@ var KeybindingEntry = GObject.registerClass({
     }
 });
 
-var ActionComboBox = GObject.registerClass({
+export const ActionComboBox = GObject.registerClass({
     Signals: { 'action-selected': { param_types: [GObject.TYPE_INT] } },
 }, class ActionComboBox extends St.Button {
     _init() {
@@ -191,7 +204,7 @@ var ActionComboBox = GObject.registerClass({
     }
 });
 
-var ActionEditor = GObject.registerClass({
+export const ActionEditor = GObject.registerClass({
     Signals: { 'done': {} },
 }, class ActionEditor extends St.Widget {
     _init() {
@@ -275,7 +288,7 @@ var ActionEditor = GObject.registerClass({
     }
 });
 
-var PadDiagram = GObject.registerClass({
+export const PadDiagram = GObject.registerClass({
     Properties: {
         'left-handed': GObject.ParamSpec.boolean('left-handed',
                                                  'left-handed', 'Left handed',
@@ -368,7 +381,7 @@ var PadDiagram = GObject.registerClass({
         let css = this._css;
 
         for (let i = 0; i < this._activeButtons.length; i++) {
-            let ch = String.fromCharCode('A'.charCodeAt() + this._activeButtons[i]);
+            let ch = String.fromCharCode('A'.charCodeAt(0) + this._activeButtons[i]);
             css += '.%s.Leader { stroke: %s !important; }'.format(ch, ACTIVE_COLOR);
             css += '.%s.Button { stroke: %s !important; fill: %s !important; }'.format(ch, ACTIVE_COLOR, 
ACTIVE_COLOR);
         }
@@ -387,7 +400,8 @@ var PadDiagram = GObject.registerClass({
         svgData += this._wrappingSvgFooter();
 
         let istream = new Gio.MemoryInputStream();
-        istream.add_bytes(new GLib.Bytes(svgData));
+        // FIXME
+        istream.add_bytes(imports.byteArray.fromString(svgData));
 
         return Rsvg.Handle.new_from_stream_sync(istream,
             Gio.File.new_for_path(this._imagePath), 0, null);
@@ -495,7 +509,7 @@ var PadDiagram = GObject.registerClass({
     }
 
     _getButtonLabels(button) {
-        let ch = String.fromCharCode('A'.charCodeAt() + button);
+        let ch = String.fromCharCode('A'.charCodeAt(0) + button);
         let labelName = 'Label%s'.format(ch);
         let leaderName = 'Leader%s'.format(ch);
         return [labelName, leaderName];
@@ -618,12 +632,19 @@ var PadDiagram = GObject.registerClass({
     }
 });
 
-var PadOsd = GObject.registerClass({
+export const PadOsd = GObject.registerClass({
     Signals: {
         'pad-selected': { param_types: [Clutter.InputDevice.$gtype] },
         'closed': {},
     },
 }, class PadOsd extends St.BoxLayout {
+    /**
+     * @param {*} padDevice 
+     * @param {*} settings 
+     * @param {*} imagePath 
+     * @param {*} editionMode 
+     * @param {*} monitorIndex 
+     */
     _init(padDevice, settings, imagePath, editionMode, monitorIndex) {
         super._init({
             style_class: 'pad-osd-window',
@@ -894,19 +915,19 @@ var PadOsd = GObject.registerClass({
     }
 
     _startButtonActionEdition(button) {
-        let ch = String.fromCharCode('A'.charCodeAt() + button);
+        let ch = String.fromCharCode('A'.charCodeAt(0) + button);
         let key = 'button%s'.format(ch);
         this._startActionEdition(key, Meta.PadActionType.BUTTON, button);
     }
 
     _startRingActionEdition(ring, dir, mode) {
-        let ch = String.fromCharCode('A'.charCodeAt() + ring);
+        let ch = String.fromCharCode('A'.charCodeAt(0) + ring);
         let key = 'ring%s-%s-mode-%d'.format(ch, dir == CCW ? 'ccw' : 'cw', mode);
         this._startActionEdition(key, Meta.PadActionType.RING, ring, dir, mode);
     }
 
     _startStripActionEdition(strip, dir, mode) {
-        let ch = String.fromCharCode('A'.charCodeAt() + strip);
+        let ch = String.fromCharCode('A'.charCodeAt(0) + strip);
         let key = 'strip%s-%s-mode-%d'.format(ch, dir == UP ? 'up' : 'down', mode);
         this._startActionEdition(key, Meta.PadActionType.STRIP, strip, dir, mode);
     }
@@ -944,7 +965,7 @@ var PadOsd = GObject.registerClass({
 
 const PadOsdIface = loadInterfaceXML('org.gnome.Shell.Wacom.PadOsd');
 
-var PadOsdService = class extends Signals.EventEmitter {
+export class PadOsdService extends Signals.EventEmitter {
     constructor() {
         super();
 
diff --git a/js/ui/pageIndicators.js b/js/ui/pageIndicators.js
index 63a31d679c..be2c6f99b8 100644
--- a/js/ui/pageIndicators.js
+++ b/js/ui/pageIndicators.js
@@ -1,14 +1,17 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported PageIndicators */
 
-const { Clutter, Graphene, GObject, St } = imports.gi;
+import Clutter from 'gi://Clutter';
+import Graphene from 'gi://Graphene';
+import GObject from 'gi://GObject';
+import St from 'gi://St';
 
 const INDICATOR_INACTIVE_OPACITY = 128;
 const INDICATOR_INACTIVE_OPACITY_HOVER = 255;
 const INDICATOR_INACTIVE_SCALE = 2 / 3;
 const INDICATOR_INACTIVE_SCALE_PRESSED = 0.5;
 
-var PageIndicators = GObject.registerClass({
+export const PageIndicators = GObject.registerClass({
     Signals: { 'page-activated': { param_types: [GObject.TYPE_INT] } },
 }, class PageIndicators extends St.BoxLayout {
     _init(orientation = Clutter.Orientation.VERTICAL) {
@@ -29,6 +32,9 @@ var PageIndicators = GObject.registerClass({
         this._orientation = orientation;
     }
 
+    /**
+     * @returns {[number, number]} 
+     */
     vfunc_get_preferred_height(forWidth) {
         // We want to request the natural height of all our children as our
         // natural height, so we chain up to St.BoxLayout, but we only request 0
diff --git a/js/ui/panel.js b/js/ui/panel.js
index dafba690a5..5af13d239b 100644
--- a/js/ui/panel.js
+++ b/js/ui/panel.js
@@ -1,23 +1,29 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported Panel */
 
-const { Atk, Clutter, GLib, GObject, Meta, Shell, St } = imports.gi;
-const Cairo = imports.cairo;
-
-const Animation = imports.ui.animation;
-const { AppMenu } = imports.ui.appMenu;
+import Atk from 'gi://Atk';
+import Clutter from 'gi://Clutter';
+import GLib from 'gi://GLib';
+import GObject from 'gi://GObject';
+import Meta from 'gi://Meta';
+import Shell from 'gi://Shell';
+import St from 'gi://St';
+import Cairo from 'cairo';
+
+import * as Animation from './animation.js';
+import { AppMenu } from './appMenu.js';
 const Config = imports.misc.config;
-const CtrlAltTab = imports.ui.ctrlAltTab;
-const DND = imports.ui.dnd;
-const Overview = imports.ui.overview;
-const PopupMenu = imports.ui.popupMenu;
-const PanelMenu = imports.ui.panelMenu;
-const Main = imports.ui.main;
+import * as CtrlAltTab from './ctrlAltTab.js';
+import * as DND from './dnd.js';
+import * as Overview from './overview.js';
+import * as PopupMenu from './popupMenu.js';
+import * as PanelMenu from './panelMenu.js';
+import Main from './main.js';
 
-var PANEL_ICON_SIZE = 16;
-var APP_MENU_ICON_MARGIN = 0;
+export let PANEL_ICON_SIZE = 16;
+export let APP_MENU_ICON_MARGIN = 0;
 
-var BUTTON_DND_ACTIVATION_TIMEOUT = 250;
+export let BUTTON_DND_ACTIVATION_TIMEOUT = 250;
 
 /**
  * AppMenuButton:
@@ -27,7 +33,7 @@ var BUTTON_DND_ACTIVATION_TIMEOUT = 250;
  * this menu also handles startup notification for it.  So when we
  * have an active startup notification, we switch modes to display that.
  */
-var AppMenuButton = GObject.registerClass({
+export const AppMenuButton = GObject.registerClass({
     Signals: { 'changed': {} },
 }, class AppMenuButton extends PanelMenu.Button {
     _init(panel) {
@@ -258,7 +264,7 @@ var AppMenuButton = GObject.registerClass({
     }
 });
 
-var ActivitiesButton = GObject.registerClass(
+export const ActivitiesButton = GObject.registerClass(
 class ActivitiesButton extends PanelMenu.Button {
     _init() {
         super._init(0.0, null, true);
@@ -294,6 +300,8 @@ class ActivitiesButton extends PanelMenu.Button {
             GLib.source_remove(this._xdndTimeOut);
         this._xdndTimeOut = GLib.timeout_add(GLib.PRIORITY_DEFAULT, BUTTON_DND_ACTIVATION_TIMEOUT, () => {
             this._xdndToggleOverview();
+
+            return false;
         });
         GLib.Source.set_name_by_id(this._xdndTimeOut, '[gnome-shell] this._xdndToggleOverview');
 
@@ -344,7 +352,7 @@ class ActivitiesButton extends PanelMenu.Button {
     }
 });
 
-var PanelCorner = GObject.registerClass(
+export const PanelCorner = GObject.registerClass(
 class PanelCorner extends St.DrawingArea {
     _init(side) {
         this._side = side;
@@ -535,6 +543,9 @@ class AggregateLayout extends Clutter.BoxLayout {
         this.layout_changed();
     }
 
+    /**
+     * @returns {[number, number]} 
+     */
     vfunc_get_preferred_width(container, forHeight) {
         let themeNode = container.get_theme_node();
         let minWidth = themeNode.get_min_width();
@@ -550,38 +561,44 @@ class AggregateLayout extends Clutter.BoxLayout {
     }
 });
 
-var AggregateMenu = GObject.registerClass(
+
+
+export const AggregateMenu = GObject.registerClass(
 class AggregateMenu extends PanelMenu.Button {
     _init() {
         super._init(0.0, C_("System menu in the top bar", "System"), false);
         this.menu.actor.add_style_class_name('aggregate-menu');
 
-        let menuLayout = new AggregateLayout();
-        this.menu.box.set_layout_manager(menuLayout);
+        this._menuLayout = new AggregateLayout();
+        this.menu.box.set_layout_manager(this._menuLayout);
 
         this._indicators = new St.BoxLayout({ style_class: 'panel-status-indicators-box' });
         this.add_child(this._indicators);
+    }
 
+    async _asyncInit() {
         if (Config.HAVE_NETWORKMANAGER)
-            this._network = new imports.ui.status.network.NMApplet();
+            this._network = new ((await import('./status/network.js')).NMApplet)();
         else
             this._network = null;
 
         if (Config.HAVE_BLUETOOTH)
-            this._bluetooth = new imports.ui.status.bluetooth.Indicator();
+            this._bluetooth = new ((await import('./status/bluetooth.js')).Indicator)();
         else
             this._bluetooth = null;
 
-        this._remoteAccess = new imports.ui.status.remoteAccess.RemoteAccessApplet();
-        this._power = new imports.ui.status.power.Indicator();
-        this._powerProfiles = new imports.ui.status.powerProfiles.Indicator();
-        this._rfkill = new imports.ui.status.rfkill.Indicator();
-        this._volume = new imports.ui.status.volume.Indicator();
-        this._brightness = new imports.ui.status.brightness.Indicator();
-        this._system = new imports.ui.status.system.Indicator();
-        this._location = new imports.ui.status.location.Indicator();
-        this._nightLight = new imports.ui.status.nightLight.Indicator();
-        this._thunderbolt = new imports.ui.status.thunderbolt.Indicator();
+        const menuLayout = this._menuLayout;
+
+        this._remoteAccess = new ((await import('./status/remoteAccess.js')).RemoteAccessApplet)();
+        this._powerProfiles = new ((await import('./status/powerProfiles.js')).Indicator)();
+        this._power = new ((await import('./status/power.js')).Indicator)();
+        this._rfkill = new ((await import('./status/rfkill.js')).Indicator)();
+        this._volume = new ((await import('./status/volume.js')).Indicator)();
+        this._brightness = new ((await import('./status/brightness.js')).Indicator)();
+        this._system = new ((await import('./status/system.js')).Indicator)();
+        this._location = new ((await import('./status/location.js')).Indicator)();
+        this._nightLight = new ((await import('./status/nightLight.js')).Indicator)();
+        this._thunderbolt = new ((await import('./status/thunderbolt.js')).Indicator)();
         this._unsafeMode = new UnsafeModeIndicator();
 
         this._indicators.add_child(this._remoteAccess);
@@ -624,17 +641,22 @@ class AggregateMenu extends PanelMenu.Button {
     }
 });
 
+import {DateMenuButton} from './dateMenu.js';
+import {ATIndicator} from './status/accessibility.js';
+import {InputSourceIndicator} from './status/keyboard.js';
+import {DwellClickIndicator} from './status/dwellClick.js';
+
 const PANEL_ITEM_IMPLEMENTATIONS = {
     'activities': ActivitiesButton,
     'aggregateMenu': AggregateMenu,
     'appMenu': AppMenuButton,
-    'dateMenu': imports.ui.dateMenu.DateMenuButton,
-    'a11y': imports.ui.status.accessibility.ATIndicator,
-    'keyboard': imports.ui.status.keyboard.InputSourceIndicator,
-    'dwellClick': imports.ui.status.dwellClick.DwellClickIndicator,
+    'dateMenu': DateMenuButton,
+    'a11y': ATIndicator,
+    'keyboard': InputSourceIndicator,
+    'dwellClick': DwellClickIndicator,
 };
 
-var Panel = GObject.registerClass(
+export const Panel = GObject.registerClass(
 class Panel extends St.Widget {
     _init() {
         super._init({ name: 'panel',
@@ -673,13 +695,26 @@ class Panel extends St.Widget {
         Main.layoutManager.panelBox.add(this);
         Main.ctrlAltTabManager.addGroup(this, _("Top Bar"), 'focus-top-bar-symbolic',
                                         { sortGroup: CtrlAltTab.SortGroup.TOP });
-
-        Main.sessionMode.connect('updated', this._updatePanel.bind(this));
+        log('updating panel...');
+        Main.sessionMode.connect('updated', () => {
+            this._updatePanel().then(() => {
+                log('Panel Updated!');
+            }).catch(err => {
+                logError(err)
+            });
+        });
 
         global.display.connect('workareas-changed', () => this.queue_relayout());
-        this._updatePanel();
+        this._updatePanel().then(() => {
+            log('Panel Updated! (1)');
+        }).catch(err => {
+            logError(err)
+        });
     }
 
+    /**
+     * @returns {[number, number]} 
+     */
     vfunc_get_preferred_width(_forHeight) {
         let primaryMonitor = Main.layoutManager.primaryMonitor;
 
@@ -863,12 +898,14 @@ class Panel extends St.Widget {
         return this._leftBox.opacity;
     }
 
-    _updatePanel() {
+    async _updatePanel() {
+        try {
+            log('Updating panel...');
         let panel = Main.sessionMode.panel;
         this._hideIndicators();
-        this._updateBox(panel.left, this._leftBox);
-        this._updateBox(panel.center, this._centerBox);
-        this._updateBox(panel.right, this._rightBox);
+        await this._updateBox(panel.left, this._leftBox);
+        await this._updateBox(panel.center, this._centerBox);
+        await this._updateBox(panel.right, this._rightBox);
 
         if (panel.left.includes('dateMenu'))
             Main.messageTray.bannerAlignment = Clutter.ActorAlign.START;
@@ -892,6 +929,10 @@ class Panel extends St.Widget {
             this._leftCorner.setStyleParent(this._leftBox);
             this._rightCorner.setStyleParent(this._rightBox);
         }
+    }catch(error) {
+        log('FAILED TO UPDATE PANEL...');
+        logError(error);
+    }
     }
 
     _hideIndicators() {
@@ -903,7 +944,7 @@ class Panel extends St.Widget {
         }
     }
 
-    _ensureIndicator(role) {
+    async _ensureIndicator(role) {
         let indicator = this.statusArea[role];
         if (!indicator) {
             let constructor = PANEL_ITEM_IMPLEMENTATIONS[role];
@@ -912,17 +953,22 @@ class Panel extends St.Widget {
                 return null;
             }
             indicator = new constructor(this);
+
+            // TODO
+            if (indicator._asyncInit)
+               await indicator._asyncInit();
+
             this.statusArea[role] = indicator;
         }
         return indicator;
     }
 
-    _updateBox(elements, box) {
+    async _updateBox(elements, box) {
         let nChildren = box.get_n_children();
 
         for (let i = 0; i < elements.length; i++) {
             let role = elements[i];
-            let indicator = this._ensureIndicator(role);
+            let indicator = await this._ensureIndicator(role);
             if (indicator == null)
                 continue;
 
diff --git a/js/ui/panelMenu.js b/js/ui/panelMenu.js
index ccfe97613c..0736b666f0 100644
--- a/js/ui/panelMenu.js
+++ b/js/ui/panelMenu.js
@@ -1,12 +1,15 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported Button, SystemIndicator */
 
-const { Atk, Clutter, GObject, St } = imports.gi;
+import Atk from 'gi://Atk';
+import Clutter from 'gi://Clutter';
+import GObject from 'gi://GObject';
+import St from 'gi://St';
 
-const Main = imports.ui.main;
-const PopupMenu = imports.ui.popupMenu;
+import Main from './main.js';
+import * as PopupMenu from './popupMenu.js';
 
-var ButtonBox = GObject.registerClass(
+export const ButtonBox = GObject.registerClass(
 class ButtonBox extends St.Widget {
     _init(params) {
         const {
@@ -34,6 +37,9 @@ class ButtonBox extends St.Widget {
         this._natHPadding = themeNode.get_length('-natural-hpadding');
     }
 
+    /**
+     * @returns {[number, number]}
+     */
     vfunc_get_preferred_width(_forHeight) {
         let child = this.get_first_child();
         let minimumSize, naturalSize;
@@ -49,6 +55,9 @@ class ButtonBox extends St.Widget {
         return [minimumSize, naturalSize];
     }
 
+    /**
+     * @returns {[number, number]}
+     */
     vfunc_get_preferred_height(_forWidth) {
         let child = this.get_first_child();
 
@@ -91,7 +100,7 @@ class ButtonBox extends St.Widget {
     }
 });
 
-var Button = GObject.registerClass({
+export const Button = GObject.registerClass({
     Signals: { 'menu-set': {} },
 }, class PanelMenuButton extends ButtonBox {
     _init(menuAlignment, nameText, dontCreateMenu) {
@@ -106,7 +115,7 @@ var Button = GObject.registerClass({
         if (dontCreateMenu)
             this.menu = new PopupMenu.PopupDummyMenu(this);
         else
-            this.setMenu(new PopupMenu.PopupMenu(this, menuAlignment, St.Side.TOP, 0));
+            this.setMenu(new PopupMenu.PopupMenu(this, menuAlignment, St.Side.TOP));
     }
 
     setSensitive(sensitive) {
@@ -180,7 +189,7 @@ var Button = GObject.registerClass({
         // measures are in logical pixels, so make sure to consider the scale
         // factor when computing max-height
         let maxHeight = Math.round((workArea.height - verticalMargins) / scaleFactor);
-        this.menu.actor.style = 'max-height: %spx;'.format(maxHeight);
+        this.menu.actor.style = 'max-height: %spx;'.format(maxHeight.toFixed(0));
     }
 
     _onDestroy() {
@@ -197,7 +206,7 @@ var Button = GObject.registerClass({
  * of an icon and a menu section, which will be composed into the
  * aggregate menu.
  */
-var SystemIndicator = GObject.registerClass(
+export const SystemIndicator = GObject.registerClass(
 class SystemIndicator extends St.BoxLayout {
     _init() {
         super._init({
diff --git a/js/ui/pointerA11yTimeout.js b/js/ui/pointerA11yTimeout.js
index 263cc3eaf2..40dd0abb5e 100644
--- a/js/ui/pointerA11yTimeout.js
+++ b/js/ui/pointerA11yTimeout.js
@@ -1,11 +1,16 @@
 /* exported PointerA11yTimeout */
-const { Clutter, GObject, Meta, St } = imports.gi;
-const Main = imports.ui.main;
-const Cairo = imports.cairo;
+import Clutter from 'gi://Clutter';
+import GObject from 'gi://GObject';
+import Meta from 'gi://Meta';
+import St from 'gi://St';
+
+import Main from './main.js';
+
+import Cairo from 'gi://cairo';
 
 const SUCCESS_ZOOM_OUT_DURATION = 150;
 
-var PieTimer = GObject.registerClass({
+export const PieTimer = GObject.registerClass({
     Properties: {
         'angle': GObject.ParamSpec.double(
             'angle', 'angle', 'angle',
@@ -106,7 +111,7 @@ var PieTimer = GObject.registerClass({
     }
 });
 
-var PointerA11yTimeout = class PointerA11yTimeout {
+export class PointerA11yTimeout {
     constructor() {
         let seat = Clutter.get_default_backend().get_default_seat();
 
diff --git a/js/ui/pointerWatcher.js b/js/ui/pointerWatcher.js
index 2af35b6173..4ab11599ab 100644
--- a/js/ui/pointerWatcher.js
+++ b/js/ui/pointerWatcher.js
@@ -1,24 +1,24 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported getPointerWatcher */
 
-const { GLib } = imports.gi;
+import GLib from 'gi://GLib';
 
 // We stop polling if the user is idle for more than this amount of time
-var IDLE_TIME = 1000;
+export const IDLE_TIME = 1000;
 
 // This file implements a reasonably efficient system for tracking the position
 // of the mouse pointer. We simply query the pointer from the X server in a loop,
 // but we turn off the polling when the user is idle.
 
 let _pointerWatcher = null;
-function getPointerWatcher() {
+export function getPointerWatcher() {
     if (_pointerWatcher == null)
         _pointerWatcher = new PointerWatcher();
 
     return _pointerWatcher;
 }
 
-var PointerWatch = class {
+export class PointerWatch {
     constructor(watcher, interval, callback) {
         this.watcher = watcher;
         this.interval = interval;
@@ -33,7 +33,7 @@ var PointerWatch = class {
     }
 };
 
-var PointerWatcher = class {
+export class PointerWatcher {
     constructor() {
         this._idleMonitor = global.backend.get_core_idle_monitor();
         this._idleMonitor.add_idle_watch(IDLE_TIME, this._onIdleMonitorBecameIdle.bind(this));
diff --git a/js/ui/popupMenu.js b/js/ui/popupMenu.js
index f4e760bfb2..e818083b28 100644
--- a/js/ui/popupMenu.js
+++ b/js/ui/popupMenu.js
@@ -3,14 +3,21 @@
             PopupImageMenuItem, PopupMenu, PopupDummyMenu, PopupSubMenu,
             PopupMenuSection, PopupSubMenuMenuItem, PopupMenuManager */
 
-const { Atk, Clutter, Gio, GObject, Graphene, Shell, St } = imports.gi;
-const Signals = imports.misc.signals;
-
-const BoxPointer = imports.ui.boxpointer;
-const GrabHelper = imports.ui.grabHelper;
-const Main = imports.ui.main;
-
-var Ornament = {
+import Atk from 'gi://Atk';
+import Clutter from 'gi://Clutter';
+import Gio from 'gi://Gio';
+import GObject from 'gi://GObject';
+import Graphene from 'gi://Graphene';
+import Shell from 'gi://Shell';
+import St from 'gi://St';
+import * as Signals from '../misc/signals.js';
+
+import * as BoxPointer from './boxpointer.js';
+import * as GrabHelper from './grabHelper.js';
+import Main from './main.js';
+
+/** @enum {number} */
+export const Ornament = {
     NONE: 0,
     DOT: 1,
     CHECK: 2,
@@ -30,7 +37,7 @@ function isPopupMenuItemVisible(child) {
  * @param {St.Side} side - Side to which the arrow points.
  * @returns {St.Icon} a new arrow icon
  */
-function arrowIcon(side) {
+export function arrowIcon(side) {
     let iconName;
     switch (side) {
     case St.Side.TOP:
@@ -56,7 +63,9 @@ function arrowIcon(side) {
     return arrow;
 }
 
-var PopupBaseMenuItem = GObject.registerClass({
+/** @typedef {{active?: boolean} & Partial<Pick<St.BoxLayout.ConstructorProperties, 'style_class' | 
'can_focus' | 'hover' | 'activate' | 'reactive'>>} PopupBaseMenuItemParams */
+
+export const PopupBaseMenuItem = GObject.registerClass({
     Properties: {
         'active': GObject.ParamSpec.boolean('active', 'active', 'active',
                                             GObject.ParamFlags.READWRITE,
@@ -69,7 +78,7 @@ var PopupBaseMenuItem = GObject.registerClass({
         'activate': { param_types: [Clutter.Event.$gtype] },
     },
 }, class PopupBaseMenuItem extends St.BoxLayout {
-    _init(params = {}) {
+    _init(params = {}, ..._) {
         const {
             reactive = true,
             activate = true,
@@ -85,6 +94,11 @@ var PopupBaseMenuItem = GObject.registerClass({
                       accessible_role: Atk.Role.MENU_ITEM });
         this._delegate = this;
 
+        /** @type {any} */
+        this.prop
+        /** @type {any} */
+        this.radioGroup
+
         this._ornament = Ornament.NONE;
         this._ornamentLabel = new St.Label({ style_class: 'popup-menu-ornament' });
         this.add(this._ornamentLabel);
@@ -267,9 +281,18 @@ var PopupBaseMenuItem = GObject.registerClass({
     }
 });
 
-var PopupMenuItem = GObject.registerClass(
+/** 
+ * @typedef {object} PopupMenuItemParams
+ * @property {string} [text]
+ * @property {boolean} [active]
+ */
+
+export const PopupMenuItem = GObject.registerClass(
 class PopupMenuItem extends PopupBaseMenuItem {
-    _init(text, params) {
+    /**
+     * @param {PopupMenuItemParams & PopupBaseMenuItemParams} [params]
+     */
+     _init(text, params) {
         super._init(params);
 
         this.label = new St.Label({ text });
@@ -279,8 +302,11 @@ class PopupMenuItem extends PopupBaseMenuItem {
 });
 
 
-var PopupSeparatorMenuItem = GObject.registerClass(
+export const PopupSeparatorMenuItem = GObject.registerClass(
 class PopupSeparatorMenuItem extends PopupBaseMenuItem {
+    /**
+     * @param {string | any} text 
+     */
     _init(text) {
         super._init({
             style_class: 'popup-separator-menu-item',
@@ -310,7 +336,7 @@ class PopupSeparatorMenuItem extends PopupBaseMenuItem {
     }
 });
 
-var Switch = GObject.registerClass({
+export const Switch = GObject.registerClass({
     Properties: {
         'state': GObject.ParamSpec.boolean(
             'state', 'state', 'state',
@@ -350,10 +376,10 @@ var Switch = GObject.registerClass({
     }
 });
 
-var PopupSwitchMenuItem = GObject.registerClass({
+export const PopupSwitchMenuItem = GObject.registerClass({
     Signals: { 'toggled': { param_types: [GObject.TYPE_BOOLEAN] } },
 }, class PopupSwitchMenuItem extends PopupBaseMenuItem {
-    _init(text, active, params) {
+     _init(text, active, params) {
         super._init(params);
 
         this.label = new St.Label({ text });
@@ -434,8 +460,13 @@ var PopupSwitchMenuItem = GObject.registerClass({
     }
 });
 
-var PopupImageMenuItem = GObject.registerClass(
+export const PopupImageMenuItem = GObject.registerClass(
 class PopupImageMenuItem extends PopupBaseMenuItem {
+    /**
+     * @param {string | any} text
+     * @param {string | Gio.Icon} [icon]
+     * @param {PopupBaseMenuItemParams} [params]
+     */
     _init(text, icon, params) {
         super._init(params);
 
@@ -446,9 +477,14 @@ class PopupImageMenuItem extends PopupBaseMenuItem {
         this.add_child(this.label);
         this.label_actor = this.label;
 
-        this.setIcon(icon);
+        if (icon) {
+            this.setIcon(icon);
+        }
     }
 
+    /**
+     * @param {string | Gio.Icon} icon 
+     */
     setIcon(icon) {
         // The 'icon' parameter can be either a Gio.Icon or a string.
         if (icon instanceof GObject.Object && GObject.type_is_a(icon, Gio.Icon))
@@ -458,7 +494,7 @@ class PopupImageMenuItem extends PopupBaseMenuItem {
     }
 });
 
-var PopupMenuBase = class extends Signals.EventEmitter {
+export class PopupMenuBase extends Signals.EventEmitter {
     constructor(sourceActor, styleClass) {
         super();
 
@@ -467,6 +503,10 @@ var PopupMenuBase = class extends Signals.EventEmitter {
 
         this.sourceActor = sourceActor;
         this.focusActor = sourceActor;
+
+        /** @type {St.Widget} */
+        this.actor;
+
         this._parent = null;
 
         this.box = new St.BoxLayout({
@@ -695,6 +735,10 @@ var PopupMenuBase = class extends Signals.EventEmitter {
         }
     }
 
+    /**
+     * @param {PopupMenuBase | PopupBaseMenuItem["prototype"]} menuItem 
+     * @param {number} [position] 
+     */
     addMenuItem(menuItem, position) {
         let beforeItem = null;
         if (position == undefined) {
@@ -774,9 +818,12 @@ var PopupMenuBase = class extends Signals.EventEmitter {
     }
 
     _getMenuItems() {
-        return this.box.get_children().map(a => a._delegate).filter(item => {
-            return item instanceof PopupBaseMenuItem || item instanceof PopupMenuSection;
-        });
+        return this.box.get_children().map(a => a._delegate).filter(
+            /**
+             * @returns {item is PopupMenuBase | PopupMenuSection}
+             */
+            item =>  item instanceof PopupBaseMenuItem || item instanceof PopupMenuSection
+        );
     }
 
     get firstMenuItem() {
@@ -791,6 +838,20 @@ var PopupMenuBase = class extends Signals.EventEmitter {
         return this._getMenuItems().length;
     }
 
+    /**
+     * @param {BoxPointer.PopupAnimation} _animation
+     */
+    open(_animation) {
+        throw new GObject.NotImplementedError(`open in ${this.constructor.name}`);
+    }
+
+    /**
+     * @param {BoxPointer.PopupAnimation} [_animation]
+     */
+    close(_animation) {
+        throw new GObject.NotImplementedError(`close in ${this.constructor.name}`);
+    }
+
     removeAll() {
         let children = this._getMenuItems();
         for (let i = 0; i < children.length; i++) {
@@ -818,7 +879,7 @@ var PopupMenuBase = class extends Signals.EventEmitter {
     }
 };
 
-var PopupMenu = class extends PopupMenuBase {
+export class PopupMenu extends PopupMenuBase {
     constructor(sourceActor, arrowAlignment, arrowSide) {
         super(sourceActor, 'popup-menu-content');
 
@@ -969,7 +1030,7 @@ var PopupMenu = class extends PopupMenuBase {
     }
 };
 
-var PopupDummyMenu = class extends Signals.EventEmitter {
+export class PopupDummyMenu extends Signals.EventEmitter {
     constructor(sourceActor) {
         super();
 
@@ -1001,7 +1062,7 @@ var PopupDummyMenu = class extends Signals.EventEmitter {
     }
 };
 
-var PopupSubMenu = class extends PopupMenuBase {
+export class PopupSubMenu extends PopupMenuBase {
     constructor(sourceActor, sourceArrow) {
         super(sourceActor);
 
@@ -1146,7 +1207,7 @@ var PopupSubMenu = class extends PopupMenuBase {
  * can add it to another menu), but is completely transparent
  * to the user
  */
-var PopupMenuSection = class extends PopupMenuBase {
+export class PopupMenuSection extends PopupMenuBase {
     constructor() {
         super();
 
@@ -1166,7 +1227,7 @@ var PopupMenuSection = class extends PopupMenuBase {
     }
 };
 
-var PopupSubMenuMenuItem = GObject.registerClass(
+export const PopupSubMenuMenuItem = GObject.registerClass(
 class PopupSubMenuMenuItem extends PopupBaseMenuItem {
     _init(text, wantIcon) {
         super._init();
@@ -1287,7 +1348,7 @@ class PopupSubMenuMenuItem extends PopupBaseMenuItem {
 /* Basic implementation of a menu manager.
  * Call addMenu to add menus
  */
-var PopupMenuManager = class {
+export class PopupMenuManager {
     constructor(owner, grabParams = {}) {
         this._grabHelper = new GrabHelper.GrabHelper(owner, {
             actionMode: Shell.ActionMode.POPUP,
diff --git a/js/ui/remoteSearch.js b/js/ui/remoteSearch.js
index 137516bdcb..72b2277656 100644
--- a/js/ui/remoteSearch.js
+++ b/js/ui/remoteSearch.js
@@ -1,9 +1,13 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported loadRemoteSearchProviders */
 
-const { GdkPixbuf, Gio, GLib, Shell, St } = imports.gi;
+import GdkPixbuf from 'gi://GdkPixbuf';
+import Gio from 'gi://Gio';
+import GLib from 'gi://GLib';
+import Shell from 'gi://Shell';
+import St from 'gi://St';
 
-const FileUtils = imports.misc.fileUtils;
+import * as FileUtils from '../misc/fileUtilsModule.js';
 
 const KEY_FILE_GROUP = 'Shell Search Provider';
 
@@ -57,10 +61,10 @@ const SearchProvider2Iface = `
 </interface>
 </node>`;
 
-var SearchProviderProxyInfo = Gio.DBusInterfaceInfo.new_for_xml(SearchProviderIface);
-var SearchProvider2ProxyInfo = Gio.DBusInterfaceInfo.new_for_xml(SearchProvider2Iface);
+export const SearchProviderProxyInfo = Gio.DBusInterfaceInfo.new_for_xml(SearchProviderIface);
+export const SearchProvider2ProxyInfo = Gio.DBusInterfaceInfo.new_for_xml(SearchProvider2Iface);
 
-function loadRemoteSearchProviders(searchSettings, callback) {
+export function loadRemoteSearchProviders(searchSettings, callback) {
     let objectPaths = {};
     let loadedProviders = [];
 
@@ -104,9 +108,9 @@ function loadRemoteSearchProviders(searchSettings, callback) {
                 // ignore error
             }
 
-            let version = '1';
+            let version = 1;
             try {
-                version = keyfile.get_string(group, 'Version');
+                version = Number.parseInt(keyfile.get_string(group, 'Version'), 10);
             } catch (e) {
                 // ignore error
             }
@@ -187,7 +191,7 @@ function loadRemoteSearchProviders(searchSettings, callback) {
     callback(loadedProviders);
 }
 
-var RemoteSearchProvider = class {
+export class RemoteSearchProvider {
     constructor(appInfo, dbusName, dbusPath, autoStart, proxyInfo) {
         if (!proxyInfo)
             proxyInfo = SearchProviderProxyInfo;
@@ -210,6 +214,7 @@ var RemoteSearchProvider = class {
         this.id = appInfo.get_id();
         this.isRemoteProvider = true;
         this.canLaunchSearch = false;
+        this.defaultEnabled = false;
     }
 
     createIcon(size, meta) {
@@ -318,7 +323,7 @@ var RemoteSearchProvider = class {
     }
 };
 
-var RemoteSearchProvider2 = class extends RemoteSearchProvider {
+export class RemoteSearchProvider2 extends RemoteSearchProvider {
     constructor(appInfo, dbusName, dbusPath, autoStart) {
         super(appInfo, dbusName, dbusPath, autoStart, SearchProvider2ProxyInfo);
 
diff --git a/js/ui/ripples.js b/js/ui/ripples.js
index f38062228f..d9d6b8707f 100644
--- a/js/ui/ripples.js
+++ b/js/ui/ripples.js
@@ -1,10 +1,11 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported Ripples */
 
-const { Clutter, St } = imports.gi;
+import Clutter from 'gi://Clutter';
+import St from 'gi://St';
 
 // Shamelessly copied from the layout "hotcorner" ripples implementation
-var Ripples = class Ripples {
+export class Ripples {
     constructor(px, py, styleClass) {
         this._x = 0;
         this._y = 0;
diff --git a/js/ui/runDialog.js b/js/ui/runDialog.js
index cf835972ea..a3cbf7ab28 100644
--- a/js/ui/runDialog.js
+++ b/js/ui/runDialog.js
@@ -1,14 +1,20 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported RunDialog */
 
-const { Clutter, Gio, GLib, GObject, Meta, Shell, St } = imports.gi;
-
-const Dialog = imports.ui.dialog;
-const Main = imports.ui.main;
-const ModalDialog = imports.ui.modalDialog;
-const ShellEntry = imports.ui.shellEntry;
-const Util = imports.misc.util;
-const History = imports.misc.history;
+import Clutter from 'gi://Clutter';
+import Gio from 'gi://Gio';
+import GLib from 'gi://GLib';
+import GObject from 'gi://GObject';
+import Meta from 'gi://Meta';
+import Shell from 'gi://Shell';
+import St from 'gi://St';
+
+import * as Dialog from './dialog.js';
+import Main from './main.js';
+import * as ModalDialog from './modalDialog.js';
+import * as ShellEntry from './shellEntry.js';
+import * as Util from '../misc/util.js';
+import * as History from '../misc/history.js';
 
 const HISTORY_KEY = 'command-history';
 
@@ -19,7 +25,7 @@ const TERMINAL_SCHEMA = 'org.gnome.desktop.default-applications.terminal';
 const EXEC_KEY = 'exec';
 const EXEC_ARG_KEY = 'exec-arg';
 
-var RunDialog = GObject.registerClass(
+export const RunDialog = GObject.registerClass(
 class RunDialog extends ModalDialog.ModalDialog {
     _init() {
         super._init({
@@ -251,6 +257,6 @@ class RunDialog extends ModalDialog.ModalDialog {
         if (this._lockdownSettings.get_boolean(DISABLE_COMMAND_LINE_KEY))
             return;
 
-        super.open();
+        return super.open();
     }
 });
diff --git a/js/ui/screenShield.js b/js/ui/screenShield.js
index 431636463a..95ee2c3c0f 100644
--- a/js/ui/screenShield.js
+++ b/js/ui/screenShield.js
@@ -1,21 +1,27 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported ScreenShield */
 
-const { AccountsService, Clutter, Gio,
-        GLib, Graphene, Meta, Shell, St } = imports.gi;
-const Signals = imports.misc.signals;
-
-const GnomeSession = imports.misc.gnomeSession;
-const OVirt = imports.gdm.oVirt;
-const LoginManager = imports.misc.loginManager;
-const Lightbox = imports.ui.lightbox;
-const Main = imports.ui.main;
-const Overview = imports.ui.overview;
-const MessageTray = imports.ui.messageTray;
-const ShellDBus = imports.ui.shellDBus;
-const SmartcardManager = imports.misc.smartcardManager;
-
-const { adjustAnimationTime } = imports.ui.environment;
+import AccountsService from 'gi://AccountsService';
+import Clutter from 'gi://Clutter';
+import Gio from 'gi://Gio';
+import GLib from 'gi://GLib';
+import Graphene from 'gi://Graphene';
+import Meta from 'gi://Meta';
+import Shell from 'gi://Shell';
+import St from 'gi://St';
+import * as Signals from '../misc/signals.js';
+
+import * as GnomeSession from '../misc/gnomeSession.js';
+import * as OVirt from '../gdm/oVirt.js';
+import * as LoginManager from '../misc/loginManager.js';
+import * as Lightbox from './lightbox.js';
+import Main from './main.js';
+import * as Overview from './overview.js';
+import * as MessageTray from './messageTray.js';
+import * as ShellDBus from './shellDBus.js';
+import * as SmartcardManager from '../misc/smartcardManager.js';
+
+import { adjustAnimationTime } from './environment.js';
 
 const SCREENSAVER_SCHEMA = 'org.gnome.desktop.screensaver';
 const LOCK_ENABLED_KEY = 'lock-enabled';
@@ -26,14 +32,15 @@ const DISABLE_LOCK_KEY = 'disable-lock-screen';
 
 const LOCKED_STATE_STR = 'screenShield.locked';
 
+
 // ScreenShield animation time
 // - STANDARD_FADE_TIME is used when the session goes idle
 // - MANUAL_FADE_TIME is used for lowering the shield when asked by the user,
 //   or when cancelling the dialog
 // - CURTAIN_SLIDE_TIME is used when raising the shield before unlocking
-var STANDARD_FADE_TIME = 10000;
-var MANUAL_FADE_TIME = 300;
-var CURTAIN_SLIDE_TIME = 300;
+export let STANDARD_FADE_TIME = 10000;
+export let MANUAL_FADE_TIME = 300;
+export let CURTAIN_SLIDE_TIME = 300;
 
 /**
  * If you are setting org.gnome.desktop.session.idle-delay directly in dconf,
@@ -43,7 +50,7 @@ var CURTAIN_SLIDE_TIME = 300;
  * This will ensure that the screen blanks at the right time when it fades out.
  * https://bugzilla.gnome.org/show_bug.cgi?id=668703 explains the dependency.
  */
-var ScreenShield = class extends Signals.EventEmitter {
+export class ScreenShield extends Signals.EventEmitter {
     constructor() {
         super();
 
@@ -71,7 +78,8 @@ var ScreenShield = class extends Signals.EventEmitter {
         this.actor.add_actor(this._lockScreenGroup);
         this.actor.add_actor(this._lockDialogGroup);
 
-        this._presence = new GnomeSession.Presence((proxy, error) => {
+        // FIXME
+        this._presence = GnomeSession.Presence((proxy, error) => {
             if (error) {
                 logError(error, 'Error while reading gnome-session presence');
                 return;
diff --git a/js/ui/screenshot.js b/js/ui/screenshot.js
index bf537b7d6d..3819938da0 100644
--- a/js/ui/screenshot.js
+++ b/js/ui/screenshot.js
@@ -1,11 +1,18 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported ScreenshotService */
 
-const { Clutter, Gio, GObject, GLib, Meta, Shell, St } = imports.gi;
+import Clutter from 'gi://Clutter';
+import Gio from 'gi://Gio';
+import GObject from 'gi://GObject';
+import GLib from 'gi://GLib';
+import Meta from 'gi://Meta';
+import Shell from 'gi://Shell';
+import St from 'gi://St';
 
-const GrabHelper = imports.ui.grabHelper;
-const Lightbox = imports.ui.lightbox;
-const Main = imports.ui.main;
+
+import * as GrabHelper from './grabHelper.js';
+import * as Lightbox from './lightbox.js';
+import Main from './main.js';
 
 Gio._promisify(Shell.Screenshot.prototype, 'pick_color', 'pick_color_finish');
 Gio._promisify(Shell.Screenshot.prototype, 'screenshot', 'screenshot_finish');
@@ -14,12 +21,12 @@ Gio._promisify(Shell.Screenshot.prototype,
 Gio._promisify(Shell.Screenshot.prototype,
     'screenshot_area', 'screenshot_area_finish');
 
-const { loadInterfaceXML } = imports.misc.fileUtils;
-const { DBusSenderChecker } = imports.misc.util;
+import { loadInterfaceXML } from '../misc/fileUtilsModule.js';
+import { DBusSenderChecker } from '../misc/util.js';
 
 const ScreenshotIface = loadInterfaceXML('org.gnome.Shell.Screenshot');
 
-var ScreenshotService = class {
+export class ScreenshotService {
     constructor() {
         this._dbusImpl = Gio.DBusExportedObject.wrapJSObject(ScreenshotIface, this);
         this._dbusImpl.export(Gio.DBus.session, '/org/gnome/Shell/Screenshot');
@@ -108,10 +115,13 @@ var ScreenshotService = class {
 
         for (let idx = 1; ; idx++) {
             yield Gio.File.new_for_path(
-                GLib.build_filenamev([path, '%s-%s.png'.format(filename, idx)]));
+                GLib.build_filenamev([path, '%s-%s.png'.format(filename, idx.toFixed(0))]));
         }
     }
 
+    /**
+     * @returns {[Gio.OutputStream, Gio.File | null] | [null, null]}
+     */
     _createStream(filename, invocation) {
         if (filename == '')
             return [Gio.MemoryOutputStream.new_resizable(), null];
@@ -182,6 +192,15 @@ var ScreenshotService = class {
         return [x, y, width, height];
     }
 
+    // FIXME
+    /**
+     * 
+     * @param {number} x 
+     * @param {number} y 
+     * @param {number} width 
+     * @param {number} height 
+     * @returns {[number, number, number, number]}
+     */
     _unscaleArea(x, y, width, height) {
         let scaleFactor = St.ThemeContext.get_for_stage(global.stage).scale_factor;
         x /= scaleFactor;
@@ -337,7 +356,7 @@ var ScreenshotService = class {
     }
 };
 
-var SelectArea = GObject.registerClass(
+export const SelectArea = GObject.registerClass(
 class SelectArea extends St.Widget {
     _init() {
         this._startX = -1;
@@ -438,7 +457,7 @@ class SelectArea extends St.Widget {
     }
 });
 
-var RecolorEffect = GObject.registerClass({
+export const RecolorEffect = GObject.registerClass({
     Properties: {
         color: GObject.ParamSpec.boxed(
             'color', 'color', 'replacement color',
@@ -567,7 +586,7 @@ var RecolorEffect = GObject.registerClass({
     }
 });
 
-var PickPixel = GObject.registerClass(
+export const PickPixel = GObject.registerClass(
 class PickPixel extends St.Widget {
     _init(screenshot) {
         super._init({ visible: false, reactive: true });
@@ -657,9 +676,9 @@ class PickPixel extends St.Widget {
     }
 });
 
-var FLASHSPOT_ANIMATION_OUT_TIME = 500; // milliseconds
+export const FLASHSPOT_ANIMATION_OUT_TIME = 500; // milliseconds
 
-var Flashspot = GObject.registerClass(
+export const Flashspot = GObject.registerClass(
 class Flashspot extends Lightbox.Lightbox {
     _init(area) {
         super._init(Main.uiGroup, {
diff --git a/js/ui/scripting.js b/js/ui/scripting.js
index aa405330c0..e5746da618 100644
--- a/js/ui/scripting.js
+++ b/js/ui/scripting.js
@@ -3,13 +3,17 @@
             destroyTestWindows, defineScriptEvent, scriptEvent,
             collectStatistics, runPerfScript */
 
-const { Gio, GLib, Meta, Shell } = imports.gi;
+import Gio from 'gi://Gio';
+import GLib from 'gi://GLib';
+import Meta from 'gi://Meta';
+import Shell from 'gi://Shell';
+
 
 const Config = imports.misc.config;
-const Main = imports.ui.main;
-const Util = imports.misc.util;
+import Main from './main.js';
+import * as Util from '../misc/util.js';
 
-const { loadInterfaceXML } = imports.misc.fileUtils;
+import { loadInterfaceXML } from '../misc/fileUtilsModule.js';
 
 // This module provides functionality for driving the shell user interface
 // in an automated fashion. The primary current use case for this is
@@ -20,7 +24,7 @@ const { loadInterfaceXML } = imports.misc.fileUtils;
 // When scripting an automated test we want to make a series of calls
 // in a linear fashion, but we also want to be able to let the main
 // loop run so actions can finish. For this reason we write the script
-// as an async function that uses await when it wants to let the main
+// as an async export function that uses await when it wants to let the main
 // loop run.
 //
 //    await Scripting.sleep(1000);
@@ -37,7 +41,7 @@ const { loadInterfaceXML } = imports.misc.fileUtils;
  * current script for the specified amount of time. Use as
  * 'yield Scripting.sleep(500);'
  */
-function sleep(milliseconds) {
+export function sleep(milliseconds) {
     return new Promise(resolve => {
         let id = GLib.timeout_add(GLib.PRIORITY_DEFAULT, milliseconds, () => {
             resolve();
@@ -55,33 +59,33 @@ function sleep(milliseconds) {
  * current script until the shell is completely idle. Use as
  * 'yield Scripting.waitLeisure();'
  */
-function waitLeisure() {
+export function waitLeisure() {
     return new Promise(resolve => {
         global.run_at_leisure(resolve);
     });
 }
 
 const PerfHelperIface = loadInterfaceXML('org.gnome.Shell.PerfHelper');
-var PerfHelperProxy = Gio.DBusProxy.makeProxyWrapper(PerfHelperIface);
-function PerfHelper() {
+export const PerfHelperProxy = Gio.DBusProxy.makeProxyWrapper(PerfHelperIface);
+export function PerfHelper() {
     return PerfHelperProxy(Gio.DBus.session, 'org.gnome.Shell.PerfHelper', '/org/gnome/Shell/PerfHelper');
 }
 
 let _perfHelper = null;
-function _getPerfHelper() {
+export function _getPerfHelper() {
     if (_perfHelper == null)
         _perfHelper = PerfHelper();
 
     return _perfHelper;
 }
 
-function _spawnPerfHelper() {
+export function _spawnPerfHelper() {
     let path = Config.LIBEXECDIR;
     let command = `${path}/gnome-shell-perf-helper`;
     Util.trySpawnCommandLine(command);
 }
 
-function _callRemote(obj, method, ...args) {
+export function _callRemote(obj, method, ...args) {
     return new Promise((resolve, reject) => {
         args.push((result, excp) => {
             if (excp)
@@ -99,18 +103,18 @@ function _callRemote(obj, method, ...args) {
  * @param {Object} params: options for window creation.
  *   {number} [params.width=640] - width of window, in pixels
  *   {number} [params.height=480] - height of window, in pixels
- *   {bool} [params.alpha=false] - whether the window should have an alpha channel
- *   {bool} [params.maximized=false] - whether the window should be created maximized
- *   {bool} [params.redraws=false] - whether the window should continually redraw itself
+ *   {boolean} [params.alpha=false] - whether the window should have an alpha channel
+ *   {boolean} [params.maximized=false] - whether the window should be created maximized
+ *   {boolean} [params.redraws=false] - whether the window should continually redraw itself
  * @returns {Promise}
  *
  * Creates a window using gnome-shell-perf-helper for testing purposes.
- * While this function can be used with yield in an automation
+ * While this export function can be used with yield in an automation
  * script to pause until the D-Bus call to the helper process returns,
  * because of the normal X asynchronous mapping process, to actually wait
  * until the window has been mapped and exposed, use waitTestWindows().
  */
-function createTestWindow(params = {}) {
+export function createTestWindow(params = {}) {
     const {
         width = 640,
         height = 480,
@@ -132,7 +136,7 @@ function createTestWindow(params = {}) {
  * Used within an automation script to pause until all windows previously
  * created with createTestWindow have been mapped and exposed.
  */
-function waitTestWindows() {
+export function waitTestWindows() {
     let perfHelper = _getPerfHelper();
     return _callRemote(perfHelper, perfHelper.WaitWindowsRemote);
 }
@@ -142,12 +146,12 @@ function waitTestWindows() {
  * @returns {Promise}
  *
  * Destroys all windows previously created with createTestWindow().
- * While this function can be used with yield in an automation
+ * While this export function can be used with yield in an automation
  * script to pause until the D-Bus call to the helper process returns,
  * this doesn't guarantee that Mutter has actually finished the destroy
  * process because of normal X asynchronicity.
  */
-function destroyTestWindows() {
+export function destroyTestWindows() {
     let perfHelper = _getPerfHelper();
     return _callRemote(perfHelper, perfHelper.DestroyWindowsRemote);
 }
@@ -161,7 +165,7 @@ function destroyTestWindows() {
  * within the 'script' namespace that is reserved for events defined locally
  * within a performance automation script
  */
-function defineScriptEvent(name, description) {
+export function defineScriptEvent(name, description) {
     Shell.PerfLog.get_default().define_event(`script.${name}`,
                                              description,
                                              "");
@@ -174,7 +178,7 @@ function defineScriptEvent(name, description) {
  * Convenience function to record a script-local performance event
  * previously defined with defineScriptEvent
  */
-function scriptEvent(name) {
+export function scriptEvent(name) {
     Shell.PerfLog.get_default().event(`script.${name}`);
 }
 
@@ -183,11 +187,11 @@ function scriptEvent(name) {
  *
  * Convenience function to trigger statistics collection
  */
-function collectStatistics() {
+export function collectStatistics() {
     Shell.PerfLog.get_default().collect_statistics();
 }
 
-function _collect(scriptModule, outputFile) {
+export function _collect(scriptModule, outputFile) {
     let eventHandlers = {};
 
     for (let f in scriptModule) {
@@ -282,7 +286,7 @@ function _collect(scriptModule, outputFile) {
     }
 }
 
-async function _runPerfScript(scriptModule, outputFile) {
+export async function _runPerfScript(scriptModule, outputFile) {
     try {
         await scriptModule.run();
     } catch (err) {
@@ -340,7 +344,7 @@ async function _runPerfScript(scriptModule, outputFile) {
  * After running the script and collecting statistics from the
  * event log, GNOME Shell will exit.
  **/
-function runPerfScript(scriptModule, outputFile) {
+export function runPerfScript(scriptModule, outputFile) {
     Shell.PerfLog.get_default().set_enabled(true);
     _spawnPerfHelper();
 
diff --git a/js/ui/search.js b/js/ui/search.js
index 7300b053e3..800fbaed4e 100644
--- a/js/ui/search.js
+++ b/js/ui/search.js
@@ -1,20 +1,26 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported SearchResultsView */
 
-const { Clutter, Gio, GLib, GObject, Meta, Shell, St } = imports.gi;
-
-const AppDisplay = imports.ui.appDisplay;
-const IconGrid = imports.ui.iconGrid;
-const Main = imports.ui.main;
-const ParentalControlsManager = imports.misc.parentalControlsManager;
-const RemoteSearch = imports.ui.remoteSearch;
-const Util = imports.misc.util;
+import Clutter from 'gi://Clutter';
+import Gio from 'gi://Gio';
+import GLib from 'gi://GLib';
+import GObject from 'gi://GObject';
+import Meta from 'gi://Meta';
+import Shell from 'gi://Shell';
+import St from 'gi://St';
+
+import * as AppDisplay from './appDisplay.js';
+import * as IconGrid from './iconGrid.js';
+import Main from './main.js';
+import * as ParentalControlsManager from '../misc/parentalControlsManager.js';
+import * as RemoteSearch from './remoteSearch.js';
+import * as Util from '../misc/util.js';
 
 const SEARCH_PROVIDERS_SCHEMA = 'org.gnome.desktop.search-providers';
 
-var MAX_LIST_SEARCH_RESULTS_ROWS = 5;
+export let MAX_LIST_SEARCH_RESULTS_ROWS = 5;
 
-var MaxWidthBox = GObject.registerClass(
+export const MaxWidthBox = GObject.registerClass(
 class MaxWidthBox extends St.BoxLayout {
     vfunc_allocate(box) {
         let themeNode = this.get_theme_node();
@@ -32,7 +38,7 @@ class MaxWidthBox extends St.BoxLayout {
     }
 });
 
-var SearchResult = GObject.registerClass(
+export const SearchResult = GObject.registerClass(
 class SearchResult extends St.Button {
     _init(provider, metaInfo, resultsView) {
         this.provider = provider;
@@ -61,8 +67,13 @@ class SearchResult extends St.Button {
     }
 });
 
-var ListSearchResult = GObject.registerClass(
+export const ListSearchResult = GObject.registerClass(
 class ListSearchResult extends SearchResult {
+    /**
+     * @param {*} provider 
+     * @param {*} metaInfo 
+     * @param {*} resultsView 
+     */
     _init(provider, metaInfo, resultsView) {
         super._init(provider, metaInfo, resultsView);
 
@@ -132,7 +143,7 @@ class ListSearchResult extends SearchResult {
     }
 });
 
-var GridSearchResult = GObject.registerClass(
+export const GridSearchResult = GObject.registerClass(
 class GridSearchResult extends SearchResult {
     _init(provider, metaInfo, resultsView) {
         super._init(provider, metaInfo, resultsView);
@@ -152,7 +163,7 @@ class GridSearchResult extends SearchResult {
     }
 });
 
-var SearchResultsBase = GObject.registerClass({
+export const SearchResultsBase = GObject.registerClass({
     GTypeFlags: GObject.TypeFlags.ABSTRACT,
     Properties: {
         'focus-child': GObject.ParamSpec.object(
@@ -194,6 +205,24 @@ var SearchResultsBase = GObject.registerClass({
         return null;
     }
 
+    _clearResultDisplay() {
+        throw new GObject.NotImplementedError(`_clearResultDisplay in ${this.constructor.name}`);
+    }
+
+    /**
+     * @returns {number}
+     */
+    _getMaxDisplayedResults() {
+        throw new GObject.NotImplementedError(`_getMaxDisplayedResults in ${this.constructor.name}`);
+    }
+
+    /**
+     * @param {*} display 
+     */
+    _addItem(display) {
+        throw new GObject.NotImplementedError(`_addItem in ${this.constructor.name}`);
+    }
+
     clear() {
         this._cancellable.cancel();
         for (let resultId in this._resultDisplays)
@@ -293,7 +322,7 @@ var SearchResultsBase = GObject.registerClass({
     }
 });
 
-var ListSearchResults = GObject.registerClass(
+export const ListSearchResults = GObject.registerClass(
 class ListSearchResults extends SearchResultsBase {
     _init(provider, resultsView) {
         super._init(provider, resultsView);
@@ -348,7 +377,7 @@ class ListSearchResults extends SearchResultsBase {
     }
 });
 
-var GridSearchResultsLayout = GObject.registerClass({
+export const GridSearchResultsLayout = GObject.registerClass({
     Properties: {
         'spacing': GObject.ParamSpec.int('spacing', 'Spacing', 'Spacing',
             GObject.ParamFlags.READWRITE, 0, GLib.MAXINT32, 0),
@@ -363,6 +392,9 @@ var GridSearchResultsLayout = GObject.registerClass({
         this._container = container;
     }
 
+    /**
+     * @returns {[number, number]}
+     */
     vfunc_get_preferred_width(container, forHeight) {
         let minWidth = 0;
         let natWidth = 0;
@@ -386,6 +418,9 @@ var GridSearchResultsLayout = GObject.registerClass({
         return [minWidth, natWidth];
     }
 
+    /**
+     * @returns {[number, number]}
+     */
     vfunc_get_preferred_height(container, forWidth) {
         let minHeight = 0;
         let natHeight = 0;
@@ -467,12 +502,12 @@ var GridSearchResultsLayout = GObject.registerClass({
     }
 });
 
-var GridSearchResults = GObject.registerClass(
+export const GridSearchResults = GObject.registerClass(
 class GridSearchResults extends SearchResultsBase {
     _init(provider, resultsView) {
         super._init(provider, resultsView);
 
-        this._grid = new St.Widget({ style_class: 'grid-search-results' });
+        this._grid = new St.Widget({ style_class: 'grid-search-results', layout_manager: /** @type 
{GridSearchResultsLayout["prototype"]} */ (null) });
         this._grid.layout_manager = new GridSearchResultsLayout();
 
         this._grid.connect('style-changed', () => {
@@ -548,7 +583,7 @@ class GridSearchResults extends SearchResultsBase {
     }
 });
 
-var SearchResultsView = GObject.registerClass({
+export const SearchResultsView = GObject.registerClass({
     Signals: { 'terms-changed': {} },
 }, class SearchResultsView extends St.BoxLayout {
     _init() {
@@ -901,7 +936,7 @@ var SearchResultsView = GObject.registerClass({
     }
 });
 
-var ProviderInfo = GObject.registerClass(
+export const ProviderInfo = GObject.registerClass(
 class ProviderInfo extends St.Button {
     _init(provider) {
         this.provider = provider;
diff --git a/js/ui/searchController.js b/js/ui/searchController.js
index 5d25897c79..b4decf69ea 100644
--- a/js/ui/searchController.js
+++ b/js/ui/searchController.js
@@ -1,13 +1,16 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported SearchController */
 
-const { Clutter, GObject, St } = imports.gi;
+import Clutter from 'gi://Clutter';
+import GObject from 'gi://GObject';
+import St from 'gi://St';
 
-const Main = imports.ui.main;
-const Search = imports.ui.search;
-const ShellEntry = imports.ui.shellEntry;
 
-var FocusTrap = GObject.registerClass(
+import Main from './main.js';
+import * as Search from './search.js';
+import * as ShellEntry from './shellEntry.js';
+
+export const FocusTrap = GObject.registerClass(
 class FocusTrap extends St.Widget {
     vfunc_navigate_focus(from, direction) {
         if (direction === St.DirectionType.TAB_FORWARD ||
@@ -24,7 +27,7 @@ function getTermsForSearchString(searchString) {
     return searchString.split(/\s+/);
 }
 
-var SearchController = GObject.registerClass({
+export const SearchController = GObject.registerClass({
     Properties: {
         'search-active': GObject.ParamSpec.boolean(
             'search-active', 'search-active', 'search-active',
diff --git a/js/ui/sessionMode.js b/js/ui/sessionMode.js
index 96136837ae..dd32e9dd0d 100644
--- a/js/ui/sessionMode.js
+++ b/js/ui/sessionMode.js
@@ -1,15 +1,18 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported SessionMode, listModes */
 
-const GLib = imports.gi.GLib;
-const Signals = imports.misc.signals;
+import GLib from 'gi://GLib';
+import * as Signals from '../misc/signals.js';
 
-const FileUtils = imports.misc.fileUtils;
+import * as FileUtils from '../misc/fileUtilsModule.js';
 
 const Config = imports.misc.config;
 
 const DEFAULT_MODE = 'restrictive';
 
+import { LoginDialog }  from "../gdm/loginDialog.js";
+import { UnlockDialog } from "../ui/unlockDialog.js";
+
 const _modes = {
     'restrictive': {
         parentMode: null,
@@ -44,7 +47,7 @@ const _modes = {
         hasNotifications: true,
         isGreeter: true,
         isPrimary: true,
-        unlockDialog: imports.gdm.loginDialog.LoginDialog,
+        unlockDialog: LoginDialog,
         components: Config.HAVE_NETWORKMANAGER
             ? ['networkAgent', 'polkitAgent']
             : ['polkitAgent'],
@@ -82,7 +85,7 @@ const _modes = {
         hasNotifications: true,
         isLocked: false,
         isPrimary: true,
-        unlockDialog: imports.ui.unlockDialog.UnlockDialog,
+        unlockDialog: UnlockDialog,
         components: Config.HAVE_NETWORKMANAGER
             ? ['networkAgent', 'polkitAgent', 'telepathyClient',
                'keyring', 'autorunManager', 'automountManager']
@@ -125,10 +128,12 @@ function _loadMode(file, info) {
 }
 
 function _loadModes() {
+    log('load modes...')
     FileUtils.collectFromDatadirs('modes', false, _loadMode);
 }
 
-function listModes() {
+export function listModes() {
+    log('list modes...')
     _loadModes();
     let loop = new GLib.MainLoop(null, false);
     let id = GLib.idle_add(GLib.PRIORITY_DEFAULT, () => {
@@ -138,12 +143,14 @@ function listModes() {
                 print(names[i]);
         }
         loop.quit();
+
+        return false;
     });
     GLib.Source.set_name_by_id(id, '[gnome-shell] listModes');
     loop.run();
 }
 
-var SessionMode = class extends Signals.EventEmitter {
+export class SessionMode extends Signals.EventEmitter {
     constructor() {
         super();
 
diff --git a/js/ui/shellDBus.js b/js/ui/shellDBus.js
index a8070eb925..0a0afd81cc 100644
--- a/js/ui/shellDBus.js
+++ b/js/ui/shellDBus.js
@@ -1,22 +1,24 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported GnomeShell, ScreenSaverDBus */
 
-const { Gio, GLib, Meta } = imports.gi;
+import Gio from 'gi://Gio';
+import GLib from 'gi://GLib';
+import Meta from 'gi://Meta';
 
-const Config = imports.misc.config;
-const ExtensionDownloader = imports.ui.extensionDownloader;
-const ExtensionUtils = imports.misc.extensionUtils;
-const Main = imports.ui.main;
-const Screenshot = imports.ui.screenshot;
 
-const { loadInterfaceXML } = imports.misc.fileUtils;
-const { DBusSenderChecker } = imports.misc.util;
-const { ControlsState } = imports.ui.overviewControls;
+const Config = imports.misc.config;
+import * as ExtensionDownloader from './extensionDownloader.js';
+import * as ExtensionUtils from '../misc/extensionUtils.js';
+import Main from './main.js';
+import * as Screenshot from './screenshot.js';
+import { ControlsState } from './overviewControls.js';
+import { DBusSenderChecker } from '../misc/util.js';
+import { loadInterfaceXML } from '../misc/fileUtilsModule.js';
 
 const GnomeShellIface = loadInterfaceXML('org.gnome.Shell');
 const ScreenSaverIface = loadInterfaceXML('org.gnome.ScreenSaver');
 
-var GnomeShell = class {
+export class GnomeShell {
     constructor() {
         this._dbusImpl = Gio.DBusExportedObject.wrapJSObject(GnomeShellIface, this);
         this._dbusImpl.export(Gio.DBus.session, '/org/gnome/Shell');
@@ -39,9 +41,9 @@ var GnomeShell = class {
 
         this._cachedOverviewVisible = false;
         Main.overview.connect('showing',
-                              this._checkOverviewVisibleChanged.bind(this));
+            this._checkOverviewVisibleChanged.bind(this));
         Main.overview.connect('hidden',
-                              this._checkOverviewVisibleChanged.bind(this));
+            this._checkOverviewVisibleChanged.bind(this));
     }
 
     /**
@@ -116,10 +118,10 @@ var GnomeShell = class {
             params[param] = params[param].deep_unpack();
 
         let { connector,
-              label,
-              level,
-              max_level: maxLevel,
-              icon: serializedIcon } = params;
+            label,
+            level,
+            max_level: maxLevel,
+            icon: serializedIcon } = params;
 
         let monitorIndex = -1;
         if (connector) {
@@ -233,7 +235,7 @@ var GnomeShell = class {
         let ungrabSucceeded = true;
 
         for (let i = 0; i < actions.length; i++)
-            ungrabSucceeded &= this._ungrabAcceleratorForSender(actions[i], sender);
+            ungrabSucceeded = ungrabSucceeded && this._ungrabAcceleratorForSender(actions[i], sender);
 
         invocation.return_value(GLib.Variant.new('(b)', [ungrabSucceeded]));
     }
@@ -274,7 +276,7 @@ var GnomeShell = class {
 
         if (!this._grabbers.has(sender)) {
             let id = Gio.bus_watch_name(Gio.BusType.SESSION, sender, 0, null,
-                                        this._onGrabberBusNameVanished.bind(this));
+                this._onGrabberBusNameVanished.bind(this));
             this._grabbers.set(sender, id);
         }
 
@@ -363,7 +365,7 @@ var GnomeShell = class {
 
 const GnomeShellExtensionsIface = loadInterfaceXML('org.gnome.Shell.Extensions');
 
-var GnomeShellExtensions = class {
+export class GnomeShellExtensions {
     constructor() {
         this._dbusImpl = Gio.DBusExportedObject.wrapJSObject(GnomeShellExtensionsIface, this);
         this._dbusImpl.export(Gio.DBus.session, '/org/gnome/Shell');
@@ -378,8 +380,8 @@ var GnomeShellExtensions = class {
                 new GLib.Variant('b', this._userExtensionsEnabled));
         });
 
-        Main.extensionManager.connect('extension-state-changed',
-                                      this._extensionStateChanged.bind(this));
+        // Main.extensionManager.connect('extension-state-changed',
+        //     this._extensionStateChanged.bind(this));
     }
 
     ListExtensions() {
@@ -460,11 +462,11 @@ var GnomeShellExtensions = class {
             new GLib.Variant('(sa{sv})', [newState.uuid, state]));
 
         this._dbusImpl.emit_signal('ExtensionStatusChanged',
-                                   GLib.Variant.new('(sis)', [newState.uuid, newState.state, 
newState.error]));
+            GLib.Variant.new('(sis)', [newState.uuid, newState.state, newState.error]));
     }
 };
 
-var ScreenSaverDBus = class {
+export class ScreenSaverDBus {
     constructor(screenShield) {
         this._screenShield = screenShield;
         screenShield.connect('active-changed', shield => {
diff --git a/js/ui/shellEntry.js b/js/ui/shellEntry.js
index f3e2b63d33..56a40f210a 100644
--- a/js/ui/shellEntry.js
+++ b/js/ui/shellEntry.js
@@ -1,13 +1,18 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported addContextMenu CapsLockWarning */
 
-const { Clutter, GObject, Pango, Shell, St } = imports.gi;
+import Clutter from 'gi://Clutter';
+import GObject from 'gi://GObject';
+import Pango from 'gi://Pango';
+import Shell from 'gi://Shell';
+import St from 'gi://St';
 
-const BoxPointer = imports.ui.boxpointer;
-const Main = imports.ui.main;
-const PopupMenu = imports.ui.popupMenu;
 
-var EntryMenu = class extends PopupMenu.PopupMenu {
+import * as BoxPointer from './boxpointer.js';
+import Main from './main.js';
+import * as PopupMenu from './popupMenu.js';
+
+export class EntryMenu extends PopupMenu.PopupMenu {
     constructor(entry) {
         super(entry, 0, St.Side.TOP);
 
@@ -126,7 +131,7 @@ function _onPopup(actor, entry) {
     entry.menu.open(BoxPointer.PopupAnimation.FULL);
 }
 
-function addContextMenu(entry, params = {}) {
+export function addContextMenu(entry, params = {}) {
     if (entry.menu)
         return;
 
@@ -156,7 +161,7 @@ function addContextMenu(entry, params = {}) {
     });
 }
 
-var CapsLockWarning = GObject.registerClass(
+export const CapsLockWarning = GObject.registerClass(
 class CapsLockWarning extends St.Label {
     _init(params) {
         let defaultParams = { style_class: 'caps-lock-warning-label' };
diff --git a/js/ui/shellMountOperation.js b/js/ui/shellMountOperation.js
index 1101c03b37..0d8bde59ee 100644
--- a/js/ui/shellMountOperation.js
+++ b/js/ui/shellMountOperation.js
@@ -1,21 +1,28 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported ShellMountOperation, GnomeShellMountOpHandler */
 
-const { Clutter, Gio, GLib, GObject, Pango, Shell, St } = imports.gi;
+import Clutter from 'gi://Clutter';
+import Gio from 'gi://Gio';
+import GLib from 'gi://GLib';
+import GObject from 'gi://GObject';
+import Pango from 'gi://Pango';
+import Shell from 'gi://Shell';
+import St from 'gi://St';
 
-const Animation = imports.ui.animation;
-const CheckBox = imports.ui.checkBox;
-const Dialog = imports.ui.dialog;
-const Main = imports.ui.main;
-const MessageTray = imports.ui.messageTray;
-const ModalDialog = imports.ui.modalDialog;
-const ShellEntry = imports.ui.shellEntry;
 
-const { loadInterfaceXML } = imports.misc.fileUtils;
-const Util = imports.misc.util;
+import * as Animation from './animation.js';
+import * as CheckBox from './checkBox.js';
+import * as Dialog from './dialog.js';
+import Main from './main.js';
+import * as MessageTray from './messageTray.js';
+import * as ModalDialog from './modalDialog.js';
+import * as ShellEntry from './shellEntry.js';
 
-var LIST_ITEM_ICON_SIZE = 48;
-var WORK_SPINNER_ICON_SIZE = 16;
+import { loadInterfaceXML } from '../misc/fileUtilsModule.js';
+import * as Util from '../misc/util.js';
+
+export let LIST_ITEM_ICON_SIZE = 48;
+export let WORK_SPINNER_ICON_SIZE = 16;
 
 const REMEMBER_MOUNT_PASSWORD_KEY = 'remember-mount-password';
 
@@ -48,7 +55,7 @@ function _setLabelsForMessage(content, message) {
 
 /* -------------------------------------------------------- */
 
-var ShellMountOperation = class {
+export class ShellMountOperation {
     constructor(source, params = {}) {
         const { existingDialog = null } = params;
 
@@ -186,7 +193,7 @@ var ShellMountOperation = class {
     }
 };
 
-var ShellUnmountNotifier = GObject.registerClass(
+export const ShellUnmountNotifier = GObject.registerClass(
 class ShellUnmountNotifier extends MessageTray.Source {
     _init() {
         super._init('', 'media-removable');
@@ -224,7 +231,7 @@ class ShellUnmountNotifier extends MessageTray.Source {
     }
 });
 
-var ShellMountQuestionDialog = GObject.registerClass({
+export const ShellMountQuestionDialog = GObject.registerClass({
     Signals: { 'response': { param_types: [GObject.TYPE_INT] } },
 }, class ShellMountQuestionDialog extends ModalDialog.ModalDialog {
     _init() {
@@ -252,7 +259,7 @@ var ShellMountQuestionDialog = GObject.registerClass({
     }
 });
 
-var ShellMountPasswordDialog = GObject.registerClass({
+export const ShellMountPasswordDialog = GObject.registerClass({
     Signals: { 'response': { param_types: [GObject.TYPE_INT,
                                            GObject.TYPE_STRING,
                                            GObject.TYPE_BOOLEAN,
@@ -415,9 +422,10 @@ var ShellMountPasswordDialog = GObject.registerClass({
     _onEntryActivate() {
         let pim = 0;
         if (this._pimEntry !== null) {
-            pim = this._pimEntry.get_text();
+            // FIXME
+            let pim_text = this._pimEntry.get_text();
 
-            if (isNaN(pim)) {
+            if (Number.isNaN(Number.parseInt(pim_text))) {
                 this._pimEntry.set_text('');
                 this._errorMessageLabel.text = _('The PIM must be a number or empty.');
                 this._errorMessageLabel.opacity = 255;
@@ -439,7 +447,7 @@ var ShellMountPasswordDialog = GObject.registerClass({
             this._hiddenVolume.checked,
             this._systemVolume &&
             this._systemVolume.checked,
-            parseInt(pim));
+            Number.parseInt(pim.toFixed(0)));
     }
 
     _onKeyfilesCheckboxClicked() {
@@ -469,7 +477,7 @@ var ShellMountPasswordDialog = GObject.registerClass({
     }
 });
 
-var ShellProcessesDialog = GObject.registerClass({
+export const ShellProcessesDialog = GObject.registerClass({
     Signals: { 'response': { param_types: [GObject.TYPE_INT] } },
 }, class ShellProcessesDialog extends ModalDialog.ModalDialog {
     _init() {
@@ -526,14 +534,15 @@ var ShellProcessesDialog = GObject.registerClass({
 
 const GnomeShellMountOpIface = loadInterfaceXML('org.Gtk.MountOperationHandler');
 
-var ShellMountOperationType = {
+/** @enum {number} */
+export const ShellMountOperationType = {
     NONE: 0,
     ASK_PASSWORD: 1,
     ASK_QUESTION: 2,
     SHOW_PROCESSES: 3,
 };
 
-var GnomeShellMountOpHandler = class {
+export class GnomeShellMountOpHandler {
     constructor() {
         this._dbusImpl = Gio.DBusExportedObject.wrapJSObject(GnomeShellMountOpIface, this);
         this._dbusImpl.export(Gio.DBus.session, '/org/gtk/MountOperationHandler');
@@ -613,7 +622,7 @@ var GnomeShellMountOpHandler = class {
     AskPasswordAsync(params, invocation) {
         let [id, message, iconName_, defaultUser_, defaultDomain_, flags] = params;
 
-        if (this._setCurrentRequest(invocation, id, ShellMountOperationType.ASK_PASSWORD)) {
+        if (this._setCurrentRequest(invocation, id, ShellMountOperationType.ASK_PASSWORD) && this._dialog 
instanceof ShellMountPasswordDialog) {
             this._dialog.reaskPassword();
             return;
         }
@@ -663,14 +672,15 @@ var GnomeShellMountOpHandler = class {
     AskQuestionAsync(params, invocation) {
         let [id, message, iconName_, choices] = params;
 
-        if (this._setCurrentRequest(invocation, id, ShellMountOperationType.ASK_QUESTION)) {
+        if (this._setCurrentRequest(invocation, id, ShellMountOperationType.ASK_QUESTION) && this._dialog 
instanceof ShellMountQuestionDialog) {
             this._dialog.update(message, choices);
             return;
         }
 
         this._closeDialog();
 
-        this._dialog = new ShellMountQuestionDialog(message);
+        // FIXME
+        this._dialog = new ShellMountQuestionDialog();
         this._dialog.connect('response', (object, choice) => {
             let response;
             let details = {};
@@ -710,7 +720,7 @@ var GnomeShellMountOpHandler = class {
     ShowProcessesAsync(params, invocation) {
         let [id, message, iconName_, applicationPids, choices] = params;
 
-        if (this._setCurrentRequest(invocation, id, ShellMountOperationType.SHOW_PROCESSES)) {
+        if (this._setCurrentRequest(invocation, id, ShellMountOperationType.SHOW_PROCESSES) && this._dialog 
instanceof ShellProcessesDialog) {
             this._dialog.update(message, applicationPids, choices);
             return;
         }
diff --git a/js/ui/slider.js b/js/ui/slider.js
index ba3a233f15..7216b0d0e7 100644
--- a/js/ui/slider.js
+++ b/js/ui/slider.js
@@ -1,13 +1,16 @@
 /* -*- mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil -*- */
 /* exported Slider */
 
-const { Atk, Clutter, GObject } = imports.gi;
+import Atk from 'gi://Atk';
+import Clutter from 'gi://Clutter';
+import GObject from 'gi://GObject';
 
-const BarLevel = imports.ui.barLevel;
 
-var SLIDER_SCROLL_STEP = 0.02; /* Slider scrolling step in % */
+import * as BarLevel from './barLevel.js';
 
-var Slider = GObject.registerClass({
+export let SLIDER_SCROLL_STEP = 0.02; /* Slider scrolling step in % */
+
+export const Slider = GObject.registerClass({
     Signals: {
         'drag-begin': {},
         'drag-end': {},
diff --git a/js/ui/status/accessibility.js b/js/ui/status/accessibility.js
index a0513f68c8..844acbc8bb 100644
--- a/js/ui/status/accessibility.js
+++ b/js/ui/status/accessibility.js
@@ -1,10 +1,14 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported ATIndicator */
 
-const { Gio, GLib, GObject, St } = imports.gi;
+import Gio from 'gi://Gio';
+import GLib from 'gi://GLib';
+import GObject from 'gi://GObject';
+import St from 'gi://St';
 
-const PanelMenu = imports.ui.panelMenu;
-const PopupMenu = imports.ui.popupMenu;
+
+import * as PanelMenu from '../panelMenu.js';
+import * as PopupMenu from '../popupMenu.js';
 
 const A11Y_SCHEMA                   = 'org.gnome.desktop.a11y';
 const KEY_ALWAYS_SHOW               = 'always-show-universal-access-status';
@@ -29,7 +33,7 @@ const KEY_TEXT_SCALING_FACTOR       = 'text-scaling-factor';
 
 const HIGH_CONTRAST_THEME           = 'HighContrast';
 
-var ATIndicator = GObject.registerClass(
+export const ATIndicator = GObject.registerClass(
 class ATIndicator extends PanelMenu.Button {
     _init() {
         super._init(0.5, _("Accessibility"));
@@ -97,6 +101,12 @@ class ATIndicator extends PanelMenu.Button {
         GLib.Source.set_name_by_id(this._syncMenuVisibilityIdle, '[gnome-shell] this._syncMenuVisibility');
     }
 
+    /**
+     * @param {string} string 
+     * @param {boolean} initialValue 
+     * @param {boolean} writable 
+     * @param {(state: boolean) => void} onSet 
+     */
     _buildItemExtended(string, initialValue, writable, onSet) {
         let widget = new PopupMenu.PopupSwitchMenuItem(string, initialValue);
         if (!writable) {
diff --git a/js/ui/status/bluetooth.js b/js/ui/status/bluetooth.js
index d4db93ea77..85c240a9bd 100644
--- a/js/ui/status/bluetooth.js
+++ b/js/ui/status/bluetooth.js
@@ -1,13 +1,17 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported Indicator */
 
-const { Gio, GLib, GnomeBluetooth, GObject } = imports.gi;
+import Gio from 'gi://Gio';
+import GLib from 'gi://GLib';
+import GnomeBluetooth from 'gi://GnomeBluetooth';
+import GObject from 'gi://GObject';
 
-const Main = imports.ui.main;
-const PanelMenu = imports.ui.panelMenu;
-const PopupMenu = imports.ui.popupMenu;
 
-const { loadInterfaceXML } = imports.misc.fileUtils;
+import Main from '../main.js';
+import * as PanelMenu from '../panelMenu.js';
+import * as PopupMenu from '../popupMenu.js';
+
+import { loadInterfaceXML } from '../../misc/fileUtilsModule.js';
 
 const BUS_NAME = 'org.gnome.SettingsDaemon.Rfkill';
 const OBJECT_PATH = '/org/gnome/SettingsDaemon/Rfkill';
@@ -17,7 +21,7 @@ const RfkillManagerProxy = Gio.DBusProxy.makeProxyWrapper(RfkillManagerInterface
 
 const HAD_BLUETOOTH_DEVICES_SETUP = 'had-bluetooth-devices-setup';
 
-var Indicator = GObject.registerClass(
+export const Indicator = GObject.registerClass(
 class Indicator extends PanelMenu.SystemIndicator {
     _init() {
         super._init();
@@ -73,10 +77,10 @@ class Indicator extends PanelMenu.SystemIndicator {
     _getDefaultAdapter() {
         let [ret, iter] = this._model.get_iter_first();
         while (ret) {
-            let isDefault = this._model.get_value(iter,
-                                                  GnomeBluetooth.Column.DEFAULT);
-            let isPowered = this._model.get_value(iter,
-                                                  GnomeBluetooth.Column.POWERED);
+            /** @type {boolean} */
+            let isDefault = (this._model.get_value(iter, GnomeBluetooth.Column.DEFAULT));
+            /** @type {boolean} */
+            let isPowered = (this._model.get_value(iter, GnomeBluetooth.Column.POWERED));
             if (isDefault && isPowered)
                 return iter;
             ret = this._model.iter_next(iter);
@@ -91,17 +95,19 @@ class Indicator extends PanelMenu.SystemIndicator {
         let deviceInfos = [];
         let [ret, iter] = this._model.iter_children(adapter);
         while (ret) {
-            const isPaired = this._model.get_value(iter,
-                GnomeBluetooth.Column.PAIRED);
-            const isTrusted = this._model.get_value(iter,
-                GnomeBluetooth.Column.TRUSTED);
+            /** @type {boolean} */
+            const isPaired = (this._model.get_value(iter,
+                GnomeBluetooth.Column.PAIRED));
+            /** @type {boolean} */
+            const isTrusted = (this._model.get_value(iter,
+                GnomeBluetooth.Column.TRUSTED));
 
             if (isPaired || isTrusted) {
                 deviceInfos.push({
-                    connected: this._model.get_value(iter,
-                        GnomeBluetooth.Column.CONNECTED),
-                    name: this._model.get_value(iter,
-                        GnomeBluetooth.Column.ALIAS),
+                    connected: /** @type {boolean} */ (this._model.get_value(iter,
+                        GnomeBluetooth.Column.CONNECTED)),
+                    name: /** @type {string} */ (this._model.get_value(iter,
+                        GnomeBluetooth.Column.ALIAS)),
                 });
             }
 
diff --git a/js/ui/status/brightness.js b/js/ui/status/brightness.js
index 2a27e78442..292dcfaac1 100644
--- a/js/ui/status/brightness.js
+++ b/js/ui/status/brightness.js
@@ -1,13 +1,15 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported Indicator */
 
-const { Gio, GObject, St } = imports.gi;
+import Gio from 'gi://Gio';
+import GObject from 'gi://GObject';
+import St from 'gi://St';
 
-const PanelMenu = imports.ui.panelMenu;
-const PopupMenu = imports.ui.popupMenu;
-const Slider = imports.ui.slider;
+import * as PanelMenu from '../panelMenu.js';
+import * as PopupMenu from '../popupMenu.js';
+import * as Slider from '../slider.js';
 
-const { loadInterfaceXML } = imports.misc.fileUtils;
+import { loadInterfaceXML } from '../../misc/fileUtilsModule.js';
 
 const BUS_NAME = 'org.gnome.SettingsDaemon.Power';
 const OBJECT_PATH = '/org/gnome/SettingsDaemon/Power';
@@ -15,7 +17,7 @@ const OBJECT_PATH = '/org/gnome/SettingsDaemon/Power';
 const BrightnessInterface = loadInterfaceXML('org.gnome.SettingsDaemon.Power.Screen');
 const BrightnessProxy = Gio.DBusProxy.makeProxyWrapper(BrightnessInterface);
 
-var Indicator = GObject.registerClass(
+export const Indicator = GObject.registerClass(
 class Indicator extends PanelMenu.SystemIndicator {
     _init() {
         super._init();
diff --git a/js/ui/status/dwellClick.js b/js/ui/status/dwellClick.js
index 4b1d8a2c13..a3c35fafc5 100644
--- a/js/ui/status/dwellClick.js
+++ b/js/ui/status/dwellClick.js
@@ -1,7 +1,11 @@
 /* exported DwellClickIndicator */
-const { Clutter, Gio, GLib, GObject, St } = imports.gi;
+import Clutter from 'gi://Clutter';
+import Gio from 'gi://Gio';
+import GLib from 'gi://GLib';
+import GObject from 'gi://GObject';
+import St from 'gi://St';
 
-const PanelMenu = imports.ui.panelMenu;
+import * as PanelMenu from '../panelMenu.js';
 
 const MOUSE_A11Y_SCHEMA       = 'org.gnome.desktop.a11y.mouse';
 const KEY_DWELL_CLICK_ENABLED = 'dwell-click-enabled';
@@ -30,7 +34,7 @@ const DWELL_CLICK_MODES = {
     },
 };
 
-var DwellClickIndicator = GObject.registerClass(
+export const DwellClickIndicator = GObject.registerClass(
 class DwellClickIndicator extends PanelMenu.Button {
     _init() {
         super._init(0.5, _("Dwell Click"));
diff --git a/js/ui/status/keyboard.js b/js/ui/status/keyboard.js
index 80a91bdfaf..c8ac086e01 100644
--- a/js/ui/status/keyboard.js
+++ b/js/ui/status/keyboard.js
@@ -1,22 +1,35 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported InputSourceIndicator */
 
-const { Clutter, Gio, GLib, GObject, IBus, Meta, Shell, St } = imports.gi;
-const Gettext = imports.gettext;
-const Signals = imports.misc.signals;
-
-const IBusManager = imports.misc.ibusManager;
-const KeyboardManager = imports.misc.keyboardManager;
-const Main = imports.ui.main;
-const PopupMenu = imports.ui.popupMenu;
-const PanelMenu = imports.ui.panelMenu;
-const SwitcherPopup = imports.ui.switcherPopup;
-const Util = imports.misc.util;
-
-var INPUT_SOURCE_TYPE_XKB = 'xkb';
-var INPUT_SOURCE_TYPE_IBUS = 'ibus';
-
-var LayoutMenuItem = GObject.registerClass(
+import Clutter from 'gi://Clutter';
+import Gio from 'gi://Gio';
+import GLib from 'gi://GLib';
+import GObject from 'gi://GObject';
+import IBus from 'gi://IBus';
+import Meta from 'gi://Meta';
+import Shell from 'gi://Shell';
+import St from 'gi://St';
+import * as Gettext from 'gettext';
+import * as Signals from '../../misc/signals.js';
+
+import * as IBusManager from '../../misc/ibusManager.js';
+import * as KeyboardManager from '../../misc/keyboardManager.js';
+import Main from '../main.js';
+import * as PopupMenu from '../popupMenu.js';
+import * as PanelMenu from '../panelMenu.js';
+import * as SwitcherPopup from '../switcherPopup.js';
+import * as Util from '../../misc/util.js';
+
+export let INPUT_SOURCE_TYPE_XKB = 'xkb';
+export let INPUT_SOURCE_TYPE_IBUS = 'ibus';
+
+/** 
+ * @typedef {object} LayoutMenuItemParams
+ * @property {string} displayName
+ * @property {string} shortName
+ */
+
+export const LayoutMenuItem = GObject.registerClass(
 class LayoutMenuItem extends PopupMenu.PopupBaseMenuItem {
     _init(displayName, shortName) {
         super._init();
@@ -32,7 +45,7 @@ class LayoutMenuItem extends PopupMenu.PopupBaseMenuItem {
     }
 });
 
-var InputSource = class extends Signals.EventEmitter {
+export class InputSource extends Signals.EventEmitter {
     constructor(type, id, displayName, shortName, index) {
         super();
 
@@ -72,8 +85,13 @@ var InputSource = class extends Signals.EventEmitter {
     }
 };
 
-var InputSourcePopup = GObject.registerClass(
+export const InputSourcePopup = GObject.registerClass(
 class InputSourcePopup extends SwitcherPopup.SwitcherPopup {
+    /**
+     * @param {*} items 
+     * @param {*} action 
+     * @param {*} actionBackward 
+     */
     _init(items, action, actionBackward) {
         super._init(items);
 
@@ -105,10 +123,13 @@ class InputSourcePopup extends SwitcherPopup.SwitcherPopup {
     }
 });
 
-var InputSourceSwitcher = GObject.registerClass(
+export const InputSourceSwitcher = GObject.registerClass(
 class InputSourceSwitcher extends SwitcherPopup.SwitcherList {
+    /**
+     * @param {*} items 
+     */
     _init(items) {
-        super._init(true);
+        super._init({ squareItems: true });
 
         for (let i = 0; i < items.length; i++)
             this._addIcon(items[i]);
@@ -136,7 +157,7 @@ class InputSourceSwitcher extends SwitcherPopup.SwitcherList {
     }
 });
 
-var InputSourceSettings = class extends Signals.EventEmitter {
+export class InputSourceSettings extends Signals.EventEmitter {
     constructor() {
         super();
 
@@ -177,7 +198,7 @@ var InputSourceSettings = class extends Signals.EventEmitter {
     }
 };
 
-var InputSourceSystemSettings = class extends InputSourceSettings {
+export class InputSourceSystemSettings extends InputSourceSettings {
     constructor() {
         super();
 
@@ -202,6 +223,7 @@ var InputSourceSystemSettings = class extends InputSourceSettings {
     }
 
     async _reload() {
+        /** @type {{ [key: string]: GLib.Variant }} */
         let props;
         try {
             const result = await Gio.DBus.system.call(
@@ -211,15 +233,19 @@ var InputSourceSystemSettings = class extends InputSourceSettings {
                 'GetAll',
                 new GLib.Variant('(s)', [this._BUS_IFACE]),
                 null, Gio.DBusCallFlags.NONE, -1, null);
-            [props] = result.deep_unpack();
+            /** @type {[{ [key: string]: GLib.Variant }]} */
+            [props] = (result.deep_unpack());
         } catch (e) {
             log('Could not get properties from %s'.format(this._BUS_NAME));
             return;
         }
 
-        const layouts = props['X11Layout'].unpack();
-        const variants = props['X11Variant'].unpack();
-        const options = props['X11Options'].unpack();
+        /** @type {string} */
+        const layouts = (props['X11Layout'].unpack());
+        /** @type {string} */
+        const variants = (props['X11Variant'].unpack());
+        /** @type {string} */
+        const options = (props['X11Options'].unpack());
 
         if (layouts !== this._layouts ||
             variants !== this._variants) {
@@ -252,7 +278,7 @@ var InputSourceSystemSettings = class extends InputSourceSettings {
     }
 };
 
-var InputSourceSessionSettings = class extends InputSourceSettings {
+export class InputSourceSessionSettings extends InputSourceSettings {
     constructor() {
         super();
 
@@ -269,12 +295,14 @@ var InputSourceSessionSettings = class extends InputSourceSettings {
     }
 
     _getSourcesList(key) {
+        /** @type {{ type: string, id: number }[]} */
         let sourcesList = [];
         let sources = this._settings.get_value(key);
         let nSources = sources.n_children();
 
         for (let i = 0; i < nSources; i++) {
-            let [type, id] = sources.get_child_value(i).deep_unpack();
+            /** @type {[string, number]} */
+            let [type, id] = (sources.get_child_value(i).deep_unpack());
             sourcesList.push({ type, id });
         }
         return sourcesList;
@@ -289,7 +317,8 @@ var InputSourceSessionSettings = class extends InputSourceSettings {
     }
 
     set mruSources(sourcesList) {
-        let sources = GLib.Variant.new('a(ss)', sourcesList);
+        let list = sourcesList.map(/** @returns {[string, string]} */ ({type, id}) => [type, id.toFixed(0)]);
+        let sources = GLib.Variant.new('a(ss)', list);
         this._settings.set_value(this._KEY_MRU_SOURCES, sources);
     }
 
@@ -302,7 +331,7 @@ var InputSourceSessionSettings = class extends InputSourceSettings {
     }
 };
 
-var InputSourceManager = class extends Signals.EventEmitter {
+export class InputSourceManager extends Signals.EventEmitter {
     constructor() {
         super();
 
@@ -782,16 +811,20 @@ var InputSourceManager = class extends Signals.EventEmitter {
     }
 };
 
+/** @type {InputSourceManager | null} */
 let _inputSourceManager = null;
 
-function getInputSourceManager() {
+export function getInputSourceManager() {
     if (_inputSourceManager == null)
         _inputSourceManager = new InputSourceManager();
     return _inputSourceManager;
 }
 
-var InputSourceIndicatorContainer = GObject.registerClass(
+export const InputSourceIndicatorContainer = GObject.registerClass(
 class InputSourceIndicatorContainer extends St.Widget {
+    /**
+     * @returns {[number, number]}
+     */
     vfunc_get_preferred_width(forHeight) {
         // Here, and in vfunc_get_preferred_height, we need to query
         // for the height of all children, but we ignore the results
@@ -803,6 +836,9 @@ class InputSourceIndicatorContainer extends St.Widget {
         }, [0, 0]);
     }
 
+    /**
+     * @returns {[number, number]}
+     */
     vfunc_get_preferred_height(forWidth) {
         return this.get_children().reduce((maxHeight, child) => {
             let height = child.get_preferred_height(forWidth);
@@ -826,7 +862,7 @@ class InputSourceIndicatorContainer extends St.Widget {
     }
 });
 
-var InputSourceIndicator = GObject.registerClass(
+export const InputSourceIndicator = GObject.registerClass(
 class InputSourceIndicator extends PanelMenu.Button {
     _init() {
         super._init(0.5, _("Keyboard"));
diff --git a/js/ui/status/location.js b/js/ui/status/location.js
index 6b399793c9..c672da6dc1 100644
--- a/js/ui/status/location.js
+++ b/js/ui/status/location.js
@@ -1,16 +1,21 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported Indicator */
 
-const { Clutter, Gio, GLib, GObject, Shell, St } = imports.gi;
-
-const Dialog = imports.ui.dialog;
-const Main = imports.ui.main;
-const PanelMenu = imports.ui.panelMenu;
-const PopupMenu = imports.ui.popupMenu;
-const ModalDialog = imports.ui.modalDialog;
-const PermissionStore = imports.misc.permissionStore;
-
-const { loadInterfaceXML } = imports.misc.fileUtils;
+import Clutter from 'gi://Clutter';
+import Gio from 'gi://Gio';
+import GLib from 'gi://GLib';
+import GObject from 'gi://GObject';
+import Shell from 'gi://Shell';
+import St from 'gi://St';
+
+import * as Dialog from '../dialog.js';
+import Main from '../main.js';
+import * as PanelMenu from '../panelMenu.js';
+import * as PopupMenu from '../popupMenu.js';
+import * as ModalDialog from '../modalDialog.js';
+import * as PermissionStore from '../../misc/permissionStore.js';
+
+import { loadInterfaceXML } from '../../misc/fileUtilsModule.js';
 
 const LOCATION_SCHEMA = 'org.gnome.system.location';
 const MAX_ACCURACY_LEVEL = 'max-accuracy-level';
@@ -19,7 +24,8 @@ const ENABLED = 'enabled';
 const APP_PERMISSIONS_TABLE = 'location';
 const APP_PERMISSIONS_ID = 'location';
 
-var GeoclueAccuracyLevel = {
+/** @enum {number} */
+export const GeoclueAccuracyLevel = {
     NONE: 0,
     COUNTRY: 1,
     CITY: 4,
@@ -37,10 +43,10 @@ function accuracyLevelToString(accuracyLevel) {
     return 'NONE';
 }
 
-var GeoclueIface = loadInterfaceXML('org.freedesktop.GeoClue2.Manager');
+export const GeoclueIface = loadInterfaceXML('org.freedesktop.GeoClue2.Manager');
 const GeoclueManager = Gio.DBusProxy.makeProxyWrapper(GeoclueIface);
 
-var AgentIface = loadInterfaceXML('org.freedesktop.GeoClue2.Agent');
+export const AgentIface = loadInterfaceXML('org.freedesktop.GeoClue2.Agent');
 
 let _geoclueAgent = null;
 function _getGeoclueAgent() {
@@ -210,7 +216,7 @@ var GeoclueAgent = GObject.registerClass({
     }
 });
 
-var Indicator = GObject.registerClass(
+export const Indicator = GObject.registerClass(
 class Indicator extends PanelMenu.SystemIndicator {
     _init() {
         super._init();
@@ -373,7 +379,8 @@ var AppAuthorizer = class {
         let dateStr = Math.round(Date.now() / 1000).toString();
         this._permissions[this.desktopId] = [levelStr, dateStr];
 
-        let data = GLib.Variant.new('av', {});
+        // FIXME
+        let data = GLib.Variant.new('av', []);
 
         this._permStoreProxy.SetRemote(APP_PERMISSIONS_TABLE,
                                        true,
@@ -387,7 +394,7 @@ var AppAuthorizer = class {
     }
 };
 
-var GeolocationDialog = GObject.registerClass({
+export const GeolocationDialog = GObject.registerClass({
     Signals: { 'response': { param_types: [GObject.TYPE_UINT] } },
 }, class GeolocationDialog extends ModalDialog.ModalDialog {
     _init(name, reason, reqAccuracyLevel) {
diff --git a/js/ui/status/network.js b/js/ui/status/network.js
index 890f9a4562..416cbb1598 100644
--- a/js/ui/status/network.js
+++ b/js/ui/status/network.js
@@ -1,25 +1,33 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported NMApplet */
-const { Clutter, Gio, GLib, GObject, Meta, NM, Polkit, St } = imports.gi;
-const Signals = imports.misc.signals;
-
-const Animation = imports.ui.animation;
-const Main = imports.ui.main;
-const PanelMenu = imports.ui.panelMenu;
-const PopupMenu = imports.ui.popupMenu;
-const MessageTray = imports.ui.messageTray;
-const ModalDialog = imports.ui.modalDialog;
-const ModemManager = imports.misc.modemManager;
-const Rfkill = imports.ui.status.rfkill;
-const Util = imports.misc.util;
-
-const { loadInterfaceXML } = imports.misc.fileUtils;
+import Clutter from 'gi://Clutter';
+import Gio from 'gi://Gio';
+import GLib from 'gi://GLib';
+import GObject from 'gi://GObject';
+import NM from 'gi://NM';
+import St from 'gi://St';
+import Polkit from 'gi://Polkit';
+import Meta from 'gi://Meta';
+import * as Signals from '../../misc/signals.js';
+
+import * as Animation from '../animation.js';
+import Main from '../main.js';
+import * as PanelMenu from '../panelMenu.js';
+import * as PopupMenu from '../popupMenu.js';
+import * as MessageTray from '../messageTray.js';
+import * as ModalDialog from '../modalDialog.js';
+import * as ModemManager from '../../misc/modemManager.js';
+import * as Rfkill from './rfkill.js';
+import * as Util from '../../misc/util.js';
+
+import { loadInterfaceXML } from '../../misc/fileUtilsModule.js';
 
 Gio._promisify(Gio.DBusConnection.prototype, 'call', 'call_finish');
 Gio._promisify(NM.Client, 'new_async', 'new_finish');
 Gio._promisify(NM.Client.prototype,
     'check_connectivity_async', 'check_connectivity_finish');
 
+/** @enum {string} */
 const NMConnectionCategory = {
     INVALID: 'invalid',
     WIRED: 'wired',
@@ -28,6 +36,7 @@ const NMConnectionCategory = {
     VPN: 'vpn',
 };
 
+/** @enum {number} */
 const NMAccessPointSecurity = {
     NONE: 1,
     WEP: 2,
@@ -37,14 +46,15 @@ const NMAccessPointSecurity = {
     WPA2_ENT: 6,
 };
 
-var MAX_DEVICE_ITEMS = 4;
+export let MAX_DEVICE_ITEMS = 4;
 
 // small optimization, to avoid using [] all the time
 const NM80211Mode = NM['80211Mode'];
 const NM80211ApFlags = NM['80211ApFlags'];
 const NM80211ApSecurityFlags = NM['80211ApSecurityFlags'];
 
-var PortalHelperResult = {
+/** @enum {number} */
+export const PortalHelperResult = {
     CANCELLED: 0,
     COMPLETED: 1,
     RECHECK: 2,
@@ -73,6 +83,9 @@ function ssidToLabel(ssid) {
     return label;
 }
 
+/**
+ * @param {NM.ActiveConnection} active 
+ */
 function ensureActiveConnectionProps(active) {
     if (!active._primaryDevice) {
         let devices = active.get_devices();
@@ -89,7 +102,7 @@ function launchSettingsPanel(panel, ...args) {
         [panel, args.map(s => new GLib.Variant('s', s))]);
     const platformData = {
         'desktop-startup-id': new GLib.Variant('s',
-            '_TIME%s'.format(global.get_current_time())),
+            '_TIME%s'.format(global.get_current_time().toFixed(0))),
     };
     try {
         Gio.DBus.session.call(
@@ -108,7 +121,7 @@ function launchSettingsPanel(panel, ...args) {
     }
 }
 
-var NMConnectionItem = class extends Signals.EventEmitter {
+export class NMConnectionItem extends Signals.EventEmitter {
     constructor(section, connection) {
         super();
 
@@ -200,14 +213,14 @@ var NMConnectionItem = class extends Signals.EventEmitter {
 
         if (this._activeConnection) {
             this._activeConnectionChangedId = this._activeConnection.connect('notify::state',
-                                                                             
this._connectionStateChanged.bind(this));
+                this._connectionStateChanged.bind(this));
         }
 
         this._sync();
     }
 };
 
-var NMConnectionSection = class NMConnectionSection extends Signals.EventEmitter {
+export class NMConnectionSection extends Signals.EventEmitter {
     constructor(client) {
         super();
 
@@ -253,10 +266,27 @@ var NMConnectionSection = class NMConnectionSection extends Signals.EventEmitter
         this.item.icon.icon_name = this._getMenuIcon();
     }
 
+    /** 
+     * @return {string}
+     */
+    _getStatus() {
+        throw new GObject.NotImplementedError(`_getStatus in ${this.constructor.name}`);
+    }
+
+    /** 
+     * @return {string}
+     */
+    getIndicatorIcon() {
+        throw new GObject.NotImplementedError(`getIndicatorIcon in ${this.constructor.name}`);
+    }
+
     _getMenuIcon() {
         return this.getIndicatorIcon();
     }
 
+    /**
+     * @returns {string}
+     */
     getConnectLabel() {
         return _("Connect");
     }
@@ -338,7 +368,12 @@ var NMConnectionSection = class NMConnectionSection extends Signals.EventEmitter
     }
 };
 
-var NMConnectionDevice = class NMConnectionDevice extends NMConnectionSection {
+
+export class NMConnectionDevice extends NMConnectionSection {
+    /**
+     * @param {NM.Client} client 
+     * @param {NM.Device} device 
+     */
     constructor(client, device) {
         super(client);
 
@@ -498,7 +533,11 @@ var NMConnectionDevice = class NMConnectionDevice extends NMConnectionSection {
     }
 };
 
-var NMDeviceWired = class extends NMConnectionDevice {
+export class NMDeviceWired extends NMConnectionDevice {
+    /**
+     * @param {NM.Client} client 
+     * @param {NM.Device} device 
+     */ 
     constructor(client, device) {
         super(client, device);
 
@@ -541,7 +580,11 @@ var NMDeviceWired = class extends NMConnectionDevice {
     }
 };
 
-var NMDeviceModem = class extends NMConnectionDevice {
+export class NMDeviceModem extends NMConnectionDevice {
+    /**
+     * @param {NM.Client} client 
+     * @param {NM.DeviceModem} device 
+     */
     constructor(client, device) {
         super(client, device);
 
@@ -647,10 +690,16 @@ var NMDeviceModem = class extends NMConnectionDevice {
     }
 };
 
-var NMDeviceBluetooth = class extends NMConnectionDevice {
+export class NMDeviceBluetooth extends NMConnectionDevice {
+    /**
+     * @param {NM.Client} client 
+     * @param {NM.DeviceBt} device 
+     */
     constructor(client, device) {
         super(client, device);
 
+        this._device = device;
+
         this.item.menu.addSettingsAction(_("Bluetooth Settings"), 'gnome-network-panel.desktop');
     }
 
@@ -681,7 +730,7 @@ var NMDeviceBluetooth = class extends NMConnectionDevice {
     }
 };
 
-var NMWirelessDialogItem = GObject.registerClass({
+export const NMWirelessDialogItem = GObject.registerClass({
     Signals: {
         'selected': {},
     },
@@ -708,7 +757,7 @@ var NMWirelessDialogItem = GObject.registerClass({
         this.add_child(this._label);
 
         this._selectedIcon = new St.Icon({ style_class: 'nm-dialog-icon',
-                                           icon_name: 'object-select-symbolic' });
+                                           icon_name: 'object-select-symbolic' })
         this.add(this._selectedIcon);
 
         this._icons = new St.BoxLayout({
@@ -755,7 +804,7 @@ var NMWirelessDialogItem = GObject.registerClass({
     }
 });
 
-var NMWirelessDialog = GObject.registerClass(
+export const NMWirelessDialog = GObject.registerClass(
 class NMWirelessDialog extends ModalDialog.ModalDialog {
     _init(client, device) {
         super._init({ styleClass: 'nm-dialog' });
@@ -1155,15 +1204,16 @@ class NMWirelessDialog extends ModalDialog.ModalDialog {
 
             this._resortItems();
         } else {
+            const ssid = accessPoint.get_ssid();
             network = {
-                ssid: accessPoint.get_ssid(),
+                ssid: ssid,
+                ssidText: ssidToLabel(ssid),
                 mode: accessPoint.mode,
                 security: this._getApSecurityType(accessPoint),
                 connections: [],
                 item: null,
                 accessPoints: [accessPoint],
             };
-            network.ssidText = ssidToLabel(network.ssid);
             this._checkConnections(network, accessPoint);
 
             let newPos = Util.insertSorted(this._networks, network, this._networkSortFunction);
@@ -1258,7 +1308,7 @@ class NMWirelessDialog extends ModalDialog.ModalDialog {
     }
 });
 
-var NMDeviceWireless = class extends Signals.EventEmitter {
+export class NMDeviceWireless extends Signals.EventEmitter {
     constructor(client, device) {
         super();
 
@@ -1471,7 +1521,7 @@ var NMDeviceWireless = class extends Signals.EventEmitter {
     }
 };
 
-var NMVpnConnectionItem = class extends NMConnectionItem {
+export class NMVpnConnectionItem extends NMConnectionItem {
     isActive() {
         if (this._activeConnection == null)
             return false;
@@ -1490,6 +1540,9 @@ var NMVpnConnectionItem = class extends NMConnectionItem {
     _sync() {
         let isActive = this.isActive();
         this.labelItem.label.text = isActive ? _("Turn Off") : this._section.getConnectLabel();
+        
+        assertType(this.radioItem, PopupMenu.PopupSwitchMenuItem);
+        
         this.radioItem.setToggleState(isActive);
         this.radioItem.setStatus(this._getStatus());
         this.emit('icon-changed');
@@ -1558,7 +1611,7 @@ var NMVpnConnectionItem = class extends NMConnectionItem {
     }
 };
 
-var NMVpnSection = class extends NMConnectionSection {
+export class NMVpnSection extends NMConnectionSection {
     constructor(client) {
         super(client);
 
@@ -1632,7 +1685,7 @@ var NMVpnSection = class extends NMConnectionSection {
     }
 };
 
-var DeviceCategory = class extends PopupMenu.PopupMenuSection {
+class DeviceCategory extends PopupMenu.PopupMenuSection {
     constructor(category) {
         super();
 
@@ -1693,7 +1746,7 @@ var DeviceCategory = class extends PopupMenu.PopupMenuSection {
     }
 };
 
-var NMApplet = GObject.registerClass(
+export const NMApplet = GObject.registerClass(
 class Indicator extends PanelMenu.SystemIndicator {
     _init() {
         super._init();
@@ -1803,7 +1856,7 @@ class Indicator extends PanelMenu.SystemIndicator {
             try {
                 this._deviceAdded(this._client, devices[i], true);
             } catch (e) {
-                log('Failed to add device %s: %s'.format(devices[i], e.toString()));
+                log('Failed to add device %s: %s'.format(devices[i].toString(), e.toString()));
             }
         }
         this._syncDeviceNames();
diff --git a/js/ui/status/nightLight.js b/js/ui/status/nightLight.js
index 7fcbfe5c24..f98b5ab2a0 100644
--- a/js/ui/status/nightLight.js
+++ b/js/ui/status/nightLight.js
@@ -1,13 +1,14 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported Indicator */
 
-const { Gio, GObject } = imports.gi;
+import Gio from 'gi://Gio';
+import GObject from 'gi://GObject';
 
-const Main = imports.ui.main;
-const PanelMenu = imports.ui.panelMenu;
-const PopupMenu = imports.ui.popupMenu;
+import Main from '../main.js';
+import * as PanelMenu from '../panelMenu.js';
+import * as PopupMenu from '../popupMenu.js';
 
-const { loadInterfaceXML } = imports.misc.fileUtils;
+import { loadInterfaceXML } from '../../misc/fileUtilsModule.js';
 
 const BUS_NAME = 'org.gnome.SettingsDaemon.Color';
 const OBJECT_PATH = '/org/gnome/SettingsDaemon/Color';
@@ -15,7 +16,7 @@ const OBJECT_PATH = '/org/gnome/SettingsDaemon/Color';
 const ColorInterface = loadInterfaceXML('org.gnome.SettingsDaemon.Color');
 const ColorProxy = Gio.DBusProxy.makeProxyWrapper(ColorInterface);
 
-var Indicator = GObject.registerClass(
+export const Indicator = GObject.registerClass(
 class Indicator extends PanelMenu.SystemIndicator {
     _init() {
         super._init();
diff --git a/js/ui/status/power.js b/js/ui/status/power.js
index ed6a46a636..ae5d9f9f7f 100644
--- a/js/ui/status/power.js
+++ b/js/ui/status/power.js
@@ -1,13 +1,17 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported Indicator */
 
-const { Clutter, Gio, GObject, St, UPowerGlib: UPower } = imports.gi;
+import Clutter from 'gi://Clutter';
+import Gio from 'gi://Gio';
+import GObject from 'gi://GObject';
+import St from 'gi://St';
+import UPower from 'gi://UPowerGlib';
 
-const Main = imports.ui.main;
-const PanelMenu = imports.ui.panelMenu;
-const PopupMenu = imports.ui.popupMenu;
+import Main from '../main.js';
+import * as PanelMenu from '../panelMenu.js';
+import * as PopupMenu from '../popupMenu.js';
 
-const { loadInterfaceXML } = imports.misc.fileUtils;
+import { loadInterfaceXML } from '../../misc/fileUtilsModule.js';
 
 const BUS_NAME = 'org.freedesktop.UPower';
 const OBJECT_PATH = '/org/freedesktop/UPower/devices/DisplayDevice';
@@ -17,7 +21,7 @@ const PowerManagerProxy = Gio.DBusProxy.makeProxyWrapper(DisplayDeviceInterface)
 
 const SHOW_BATTERY_PERCENTAGE       = 'show-battery-percentage';
 
-var Indicator = GObject.registerClass(
+export const Indicator = GObject.registerClass(
 class Indicator extends PanelMenu.SystemIndicator {
     _init() {
         super._init();
diff --git a/js/ui/status/powerProfiles.js b/js/ui/status/powerProfiles.js
index 61205bbc60..649b7ebfcc 100644
--- a/js/ui/status/powerProfiles.js
+++ b/js/ui/status/powerProfiles.js
@@ -3,11 +3,11 @@
 
 const { Gio, GObject } = imports.gi;
 
-const Main = imports.ui.main;
-const PanelMenu = imports.ui.panelMenu;
-const PopupMenu = imports.ui.popupMenu;
+import Main from '../main.js';
+import * as PanelMenu from '../panelMenu.js';
+import * as PopupMenu from '../popupMenu.js';
 
-const { loadInterfaceXML } = imports.misc.fileUtils;
+import { loadInterfaceXML } from '../../misc/fileUtilsModule.js';
 
 const BUS_NAME = 'net.hadess.PowerProfiles';
 const OBJECT_PATH = '/net/hadess/PowerProfiles';
@@ -26,7 +26,7 @@ const PROFILE_ICONS = {
     'power-saver': 'power-profile-power-saver-symbolic',
 };
 
-var Indicator = GObject.registerClass(
+export const Indicator = GObject.registerClass(
 class Indicator extends PanelMenu.SystemIndicator {
     _init() {
         super._init();
diff --git a/js/ui/status/remoteAccess.js b/js/ui/status/remoteAccess.js
index 21f6581b61..18d41acc90 100644
--- a/js/ui/status/remoteAccess.js
+++ b/js/ui/status/remoteAccess.js
@@ -1,12 +1,13 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported RemoteAccessApplet */
 
-const { GObject, Meta } = imports.gi;
+import GObject from 'gi://GObject';
+import Meta from 'gi://Meta';
 
-const PanelMenu = imports.ui.panelMenu;
-const PopupMenu = imports.ui.popupMenu;
+import * as PanelMenu from '../panelMenu.js';
+import * as PopupMenu from '../popupMenu.js';
 
-var RemoteAccessApplet = GObject.registerClass(
+export const RemoteAccessApplet = GObject.registerClass(
 class RemoteAccessApplet extends PanelMenu.SystemIndicator {
     _init() {
         super._init();
diff --git a/js/ui/status/rfkill.js b/js/ui/status/rfkill.js
index c090ffd004..d296c71f15 100644
--- a/js/ui/status/rfkill.js
+++ b/js/ui/status/rfkill.js
@@ -1,14 +1,15 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported Indicator */
 
-const { Gio, GObject } = imports.gi;
-const Signals = imports.misc.signals;
+import Gio from 'gi://Gio';
+import GObject from 'gi://GObject';
+import * as Signals from '../../misc/signals.js';
 
-const Main = imports.ui.main;
-const PanelMenu = imports.ui.panelMenu;
-const PopupMenu = imports.ui.popupMenu;
+import Main from '../main.js';
+import * as PanelMenu from '../panelMenu.js';
+import * as PopupMenu from '../popupMenu.js';
 
-const { loadInterfaceXML } = imports.misc.fileUtils;
+import { loadInterfaceXML } from '../../misc/fileUtilsModule.js';
 
 const BUS_NAME = 'org.gnome.SettingsDaemon.Rfkill';
 const OBJECT_PATH = '/org/gnome/SettingsDaemon/Rfkill';
@@ -54,7 +55,7 @@ var RfkillManager = class extends Signals.EventEmitter {
 };
 
 var _manager;
-function getRfkillManager() {
+export function getRfkillManager() {
     if (_manager != null)
         return _manager;
 
@@ -62,7 +63,7 @@ function getRfkillManager() {
     return _manager;
 }
 
-var Indicator = GObject.registerClass(
+export const Indicator = GObject.registerClass(
 class Indicator extends PanelMenu.SystemIndicator {
     _init() {
         super._init();
diff --git a/js/ui/status/system.js b/js/ui/status/system.js
index 6f71109c52..7c2547753f 100644
--- a/js/ui/status/system.js
+++ b/js/ui/status/system.js
@@ -1,21 +1,23 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported Indicator */
 
-const { GObject, Shell, St } = imports.gi;
+import GObject from 'gi://GObject';
+import Shell from 'gi://Shell';
+import St from 'gi://St';
 
-const BoxPointer = imports.ui.boxpointer;
-const SystemActions = imports.misc.systemActions;
-const Main = imports.ui.main;
-const PanelMenu = imports.ui.panelMenu;
-const PopupMenu = imports.ui.popupMenu;
+import * as BoxPointer from '../boxpointer.js';
+import * as SystemActions from '../../misc/systemActions.js';
+import Main from '../main.js';
+import * as PanelMenu from '../panelMenu.js';
+import * as PopupMenu from '../popupMenu.js';
 
 
-var Indicator = GObject.registerClass(
+export const Indicator = GObject.registerClass(
 class Indicator extends PanelMenu.SystemIndicator {
     _init() {
         super._init();
 
-        this._systemActions = new SystemActions.getDefault();
+        this._systemActions = SystemActions.getDefault();
 
         this._createSubMenu();
 
diff --git a/js/ui/status/thunderbolt.js b/js/ui/status/thunderbolt.js
index 12535394e7..c0176efa6b 100644
--- a/js/ui/status/thunderbolt.js
+++ b/js/ui/status/thunderbolt.js
@@ -3,14 +3,18 @@
 
 // the following is a modified version of bolt/contrib/js/client.js
 
-const { Gio, GLib, GObject, Polkit, Shell } = imports.gi;
-const Signals = imports.misc.signals;
+import Gio from 'gi://Gio';
+import GLib from 'gi://GLib';
+import GObject from 'gi://GObject';
+import Polkit from 'gi://Polkit';
+import Shell from 'gi://Shell';
+import * as Signals from '../../misc/signals.js';
 
-const Main = imports.ui.main;
-const MessageTray = imports.ui.messageTray;
-const PanelMenu = imports.ui.panelMenu;
+import Main from '../main.js';
+import * as MessageTray from '../messageTray.js';
+import * as PanelMenu from '../panelMenu.js';
 
-const { loadInterfaceXML } = imports.misc.fileUtils;
+import { loadInterfaceXML } from '../../misc/fileUtilsModule.js';
 
 /* Keep in sync with data/org.freedesktop.bolt.xml */
 
@@ -21,7 +25,7 @@ const BoltDeviceProxy = Gio.DBusProxy.makeProxyWrapper(BoltDeviceInterface);
 
 /*  */
 
-var Status = {
+export const Status = {
     DISCONNECTED: 'disconnected',
     CONNECTING: 'connecting',
     CONNECTED: 'connected',
@@ -30,17 +34,17 @@ var Status = {
     AUTHORIZED: 'authorized',
 };
 
-var Policy = {
+export const Policy = {
     DEFAULT: 'default',
     MANUAL: 'manual',
     AUTO: 'auto',
 };
 
-var AuthCtrl = {
+export const AuthCtrl = {
     NONE: 'none',
 };
 
-var AuthMode = {
+export const AuthMode = {
     DISABLED: 'disabled',
     ENABLED: 'enabled',
 };
@@ -49,7 +53,7 @@ const BOLT_DBUS_CLIENT_IFACE = 'org.freedesktop.bolt1.Manager';
 const BOLT_DBUS_NAME = 'org.freedesktop.bolt';
 const BOLT_DBUS_PATH = '/org/freedesktop/bolt';
 
-var Client = class extends Signals.EventEmitter {
+export class Client extends Signals.EventEmitter {
     constructor() {
         super();
 
@@ -130,7 +134,7 @@ var Client = class extends Signals.EventEmitter {
 };
 
 /* helper class to automatically authorize new devices */
-var AuthRobot = class extends Signals.EventEmitter {
+export class AuthRobot extends Signals.EventEmitter {
     constructor(client) {
         super();
 
@@ -222,7 +226,7 @@ var AuthRobot = class extends Signals.EventEmitter {
 
 /* eof client.js  */
 
-var Indicator = GObject.registerClass(
+export const Indicator = GObject.registerClass(
 class Indicator extends PanelMenu.SystemIndicator {
     _init() {
         super._init();
diff --git a/js/ui/status/volume.js b/js/ui/status/volume.js
index d71daadc5c..9ca7bd90ac 100644
--- a/js/ui/status/volume.js
+++ b/js/ui/status/volume.js
@@ -1,13 +1,18 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported Indicator */
 
-const { Clutter, Gio, GLib, GObject, Gvc, St } = imports.gi;
-const Signals = imports.misc.signals;
-
-const Main = imports.ui.main;
-const PanelMenu = imports.ui.panelMenu;
-const PopupMenu = imports.ui.popupMenu;
-const Slider = imports.ui.slider;
+import Clutter from 'gi://Clutter';
+import Gio from 'gi://Gio';
+import GLib from 'gi://GLib';
+import GObject from 'gi://GObject';
+import Gvc from 'gi://Gvc';
+import St from 'gi://St';
+import * as Signals from '../../misc/signals.js';
+
+import Main from '../main.js';
+import * as PanelMenu from '../panelMenu.js';
+import * as PopupMenu from '../popupMenu.js';
+import * as Slider from '../slider.js';
 
 const ALLOW_AMPLIFIED_VOLUME_KEY = 'allow-volume-above-100-percent';
 
@@ -19,7 +24,7 @@ const VolumeType = {
 // Each Gvc.MixerControl is a connection to PulseAudio,
 // so it's better to make it a singleton
 let _mixerControl;
-function getMixerControl() {
+export function getMixerControl() {
     if (_mixerControl)
         return _mixerControl;
 
@@ -29,7 +34,7 @@ function getMixerControl() {
     return _mixerControl;
 }
 
-var StreamSlider = class extends Signals.EventEmitter {
+export class StreamSlider extends Signals.EventEmitter {
     constructor(control) {
         super();
 
@@ -217,7 +222,7 @@ var StreamSlider = class extends Signals.EventEmitter {
     }
 };
 
-var OutputStreamSlider = class extends StreamSlider {
+export class OutputStreamSlider extends StreamSlider {
     constructor(control) {
         super(control);
         this._slider.accessible_name = _("Volume");
@@ -272,7 +277,7 @@ var OutputStreamSlider = class extends StreamSlider {
     }
 };
 
-var InputStreamSlider = class extends StreamSlider {
+export class InputStreamSlider extends StreamSlider {
     constructor(control) {
         super(control);
         this._slider.accessible_name = _("Microphone");
@@ -317,7 +322,7 @@ var InputStreamSlider = class extends StreamSlider {
     }
 };
 
-var VolumeMenu = class extends PopupMenu.PopupMenuSection {
+export class VolumeMenu extends PopupMenu.PopupMenuSection {
     constructor(control) {
         super();
 
@@ -394,7 +399,7 @@ var VolumeMenu = class extends PopupMenu.PopupMenuSection {
     }
 };
 
-var Indicator = GObject.registerClass(
+export const Indicator = GObject.registerClass(
 class Indicator extends PanelMenu.SystemIndicator {
     _init() {
         super._init();
diff --git a/js/ui/swipeTracker.js b/js/ui/swipeTracker.js
index 8d96c2dd87..975cae3189 100644
--- a/js/ui/swipeTracker.js
+++ b/js/ui/swipeTracker.js
@@ -1,9 +1,12 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported SwipeTracker */
 
-const { Clutter, Gio, GObject, Meta } = imports.gi;
+import Clutter from 'gi://Clutter';
+import Gio from 'gi://Gio';
+import GObject from 'gi://GObject';
+import Meta from 'gi://Meta';
 
-const Main = imports.ui.main;
+import Main from './main.js';
 
 // FIXME: ideally these values matches physical touchpad size. We can get the
 // correct values for gnome-shell specifically, since mutter uses libinput
@@ -33,6 +36,7 @@ const EPSILON = 0.005;
 
 const GESTURE_FINGER_COUNT = 3;
 
+/** @enum {number} */
 const State = {
     NONE: 0,
     SCROLLING: 1,
@@ -107,6 +111,12 @@ const TouchpadSwipeGesture = GObject.registerClass({
         this._state = TouchpadState.NONE;
         this._cumulativeX = 0;
         this._cumulativeY = 0;
+
+        /** @type {Clutter.Orientation} */
+        this.orientation;
+        /** @type {boolean} */
+        this.enabled;
+
         this._touchpadSettings = new Gio.Settings({
             schema_id: 'org.gnome.desktop.peripherals.touchpad',
         });
@@ -232,6 +242,9 @@ const TouchSwipeGesture = GObject.registerClass({
         this.set_n_touch_points(nTouchPoints);
         this.set_threshold_trigger_edge(thresholdTriggerEdge);
 
+        /** @type {Clutter.Orientation} */
+        this.orientation;
+
         this._allowedModes = allowedModes;
         this._distance = global.screen_height;
 
@@ -332,6 +345,11 @@ const ScrollGesture = GObject.registerClass({
         this._began = false;
         this._enabled = true;
 
+        /** @type {Clutter.Orientation} */
+        this.orientation;
+        /** @type {Clutter.ModifierType} */
+        this.scrollModifiers;
+
         actor.connect('scroll-event', this._handleEvent.bind(this));
     }
 
@@ -432,8 +450,14 @@ const ScrollGesture = GObject.registerClass({
 //   NOTE: duration can be 0 in some cases, in this case it should finish
 //   instantly.
 
+/** 
+ * @typedef {object} SwipeTrackerParams
+ * @property {boolean} allowDrag
+ * @property {boolean} allowScroll
+ */
+
 /** A class for handling swipe gestures */
-var SwipeTracker = GObject.registerClass({
+export const SwipeTracker = GObject.registerClass({
     Properties: {
         'enabled': GObject.ParamSpec.boolean(
             'enabled', 'enabled', 'enabled',
@@ -466,6 +490,9 @@ var SwipeTracker = GObject.registerClass({
         super._init();
         const { allowDrag = true, allowScroll = true } = params;
 
+        /** @type {boolean} */
+        this.allowLongSwipes
+
         this.orientation = orientation;
         this._allowedModes = allowedModes;
         this._enabled = true;
@@ -528,7 +555,7 @@ var SwipeTracker = GObject.registerClass({
     /**
      * canHandleScrollEvent:
      * @param {Clutter.Event} scrollEvent: an event to check
-     * @returns {bool} whether the event can be handled by the tracker
+     * @returns {boolean} whether the event can be handled by the tracker
      *
      * This function can be used to combine swipe gesture and mouse
      * scrolling.
@@ -631,6 +658,9 @@ var SwipeTracker = GObject.registerClass({
         return this._findClosestPoint(pos);
     }
 
+    /**
+     * @returns {[number, number]} 
+     */
     _getBounds(pos) {
         if (this.allowLongSwipes)
             return [this._snapPoints[0], this._snapPoints[this._snapPoints.length - 1]];
diff --git a/js/ui/switchMonitor.js b/js/ui/switchMonitor.js
index 5ac5825224..50bb2098ed 100644
--- a/js/ui/switchMonitor.js
+++ b/js/ui/switchMonitor.js
@@ -1,13 +1,17 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported SwitchMonitorPopup */
 
-const { Clutter, GObject, Meta, St } = imports.gi;
+import Clutter from 'gi://Clutter';
+import GObject from 'gi://GObject';
+import Meta from 'gi://Meta';
+import St from 'gi://St';
 
-const SwitcherPopup = imports.ui.switcherPopup;
 
-var APP_ICON_SIZE = 96;
+import * as SwitcherPopup from './switcherPopup.js';
 
-var SwitchMonitorPopup = GObject.registerClass(
+export let APP_ICON_SIZE = 96;
+
+export const SwitchMonitorPopup = GObject.registerClass(
 class SwitchMonitorPopup extends SwitcherPopup.SwitcherPopup {
     _init() {
         let items = [{ icon: 'view-mirror-symbolic',
@@ -69,10 +73,13 @@ class SwitchMonitorPopup extends SwitcherPopup.SwitcherPopup {
     }
 });
 
-var SwitchMonitorSwitcher = GObject.registerClass(
+export const SwitchMonitorSwitcher = GObject.registerClass(
 class SwitchMonitorSwitcher extends SwitcherPopup.SwitcherList {
+    /**
+     * @param {*} items 
+     */
     _init(items) {
-        super._init(true);
+        super._init({ squareItems: true });
 
         for (let i = 0; i < items.length; i++)
             this._addIcon(items[i]);
diff --git a/js/ui/switcherPopup.js b/js/ui/switcherPopup.js
index b1e2730068..209b8507c6 100644
--- a/js/ui/switcherPopup.js
+++ b/js/ui/switcherPopup.js
@@ -1,19 +1,24 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported SwitcherPopup, SwitcherList */
 
-const { Clutter, GLib, GObject, Meta, St } = imports.gi;
+import Clutter from 'gi://Clutter';
+import GLib from 'gi://GLib';
+import GObject from 'gi://GObject';
+import Meta from 'gi://Meta';
+import St from 'gi://St';
 
-const Main = imports.ui.main;
 
-var POPUP_DELAY_TIMEOUT = 150; // milliseconds
+import Main from './main.js';
 
-var POPUP_SCROLL_TIME = 100; // milliseconds
-var POPUP_FADE_OUT_TIME = 100; // milliseconds
+export let POPUP_DELAY_TIMEOUT = 150; // milliseconds
 
-var DISABLE_HOVER_TIMEOUT = 500; // milliseconds
-var NO_MODS_TIMEOUT = 1500; // milliseconds
+export let POPUP_SCROLL_TIME = 100; // milliseconds
+export let POPUP_FADE_OUT_TIME = 100; // milliseconds
 
-function mod(a, b) {
+export let DISABLE_HOVER_TIMEOUT = 500; // milliseconds
+export let NO_MODS_TIMEOUT = 1500; // milliseconds
+
+export function mod(a, b) {
     return (a + b) % b;
 }
 
@@ -29,10 +34,10 @@ function primaryModifier(mask) {
     return primary;
 }
 
-var SwitcherPopup = GObject.registerClass({
+export const SwitcherPopup = GObject.registerClass({
     GTypeFlags: GObject.TypeFlags.ABSTRACT,
 }, class SwitcherPopup extends St.Widget {
-    _init(items) {
+    _init(items, ..._) {
         super._init({ style_class: 'switcher-popup',
                       reactive: true,
                       visible: false });
@@ -168,6 +173,11 @@ var SwitcherPopup = GObject.registerClass({
         return mod(this._selectedIndex - 1, this._items.length);
     }
 
+    /**
+     * @param {*} _keysym 
+     * @param {*} _action 
+     * @returns {boolean}
+     */
     _keyPressHandler(_keysym, _action) {
         throw new GObject.NotImplementedError(`_keyPressHandler in ${this.constructor.name}`);
     }
@@ -353,7 +363,7 @@ var SwitcherPopup = GObject.registerClass({
     }
 });
 
-var SwitcherButton = GObject.registerClass(
+export const SwitcherButton = GObject.registerClass(
 class SwitcherButton extends St.Button {
     _init(square) {
         super._init({ style_class: 'item-box',
@@ -370,7 +380,7 @@ class SwitcherButton extends St.Button {
     }
 });
 
-var SwitcherList = GObject.registerClass({
+export const SwitcherList = GObject.registerClass({
     Signals: { 'item-activated': { param_types: [GObject.TYPE_INT] },
                'item-entered': { param_types: [GObject.TYPE_INT] },
                'item-removed': { param_types: [GObject.TYPE_INT] } },
@@ -627,7 +637,7 @@ var SwitcherList = GObject.registerClass({
     }
 });
 
-function drawArrow(area, side) {
+export function drawArrow(area, side) {
     let themeNode = area.get_theme_node();
     let borderColor = themeNode.get_border_color(side);
     let bodyColor = themeNode.get_foreground_color();
diff --git a/js/ui/unlockDialog.js b/js/ui/unlockDialog.js
index 370385abc4..3250878c8b 100644
--- a/js/ui/unlockDialog.js
+++ b/js/ui/unlockDialog.js
@@ -1,16 +1,26 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported UnlockDialog */
 
-const { AccountsService, Atk, Clutter, Gdm, Gio,
-        GnomeDesktop, GLib, GObject, Meta, Shell, St } = imports.gi;
-
-const Background = imports.ui.background;
-const Layout = imports.ui.layout;
-const Main = imports.ui.main;
-const MessageTray = imports.ui.messageTray;
-const SwipeTracker = imports.ui.swipeTracker;
-
-const AuthPrompt = imports.gdm.authPrompt;
+import AccountsService from 'gi://AccountsService';
+import Atk from 'gi://Atk';
+import Clutter from 'gi://Clutter';
+import Gdm from 'gi://Gdm';
+import Gio from 'gi://Gio';
+import GnomeDesktop from 'gi://GnomeDesktop';
+import GLib from 'gi://GLib';
+import GObject from 'gi://GObject';
+import Meta from 'gi://Meta';
+import Shell from 'gi://Shell';
+import St from 'gi://St';
+
+
+import * as Background from './background.js';
+import * as Layout from './layout.js';
+import Main from './main.js';
+import * as MessageTray from './messageTray.js';
+import * as SwipeTracker from './swipeTracker.js';
+
+import * as AuthPrompt from '../gdm/authPrompt.js';
 
 // The timeout before going back automatically to the lock screen (in seconds)
 const IDLE_TIMEOUT = 2 * 60;
@@ -27,7 +37,7 @@ const BLUR_SIGMA = 60;
 
 const SUMMARY_ICON_SIZE = 32;
 
-var NotificationsBox = GObject.registerClass({
+export const NotificationsBox = GObject.registerClass({
     Signals: { 'wake-up-screen': {} },
 }, class NotificationsBox extends St.BoxLayout {
     _init() {
@@ -314,7 +324,7 @@ var NotificationsBox = GObject.registerClass({
     }
 });
 
-var Clock = GObject.registerClass(
+export const Clock = GObject.registerClass(
 class UnlockDialogClock extends St.BoxLayout {
     _init() {
         super._init({ style_class: 'unlock-dialog-clock', vertical: true });
@@ -387,8 +397,13 @@ class UnlockDialogClock extends St.BoxLayout {
     }
 });
 
-var UnlockDialogLayout = GObject.registerClass(
+export const UnlockDialogLayout = GObject.registerClass(
 class UnlockDialogLayout extends Clutter.LayoutManager {
+    /**
+     * @param {*} stack 
+     * @param {*} notifications 
+     * @param {*} switchUserButton 
+     */
     _init(stack, notifications, switchUserButton) {
         super._init();
 
@@ -466,12 +481,15 @@ class UnlockDialogLayout extends Clutter.LayoutManager {
     }
 });
 
-var UnlockDialog = GObject.registerClass({
+export const UnlockDialog = GObject.registerClass({
     Signals: {
         'failed': {},
         'wake-up-screen': {},
     },
 }, class UnlockDialog extends St.Widget {
+    /**
+     * @param {Clutter.Actor} parentActor 
+     */
     _init(parentActor) {
         super._init({
             accessible_role: Atk.Role.WINDOW,
diff --git a/js/ui/userWidget.js b/js/ui/userWidget.js
index bfc91e2766..88b0bd8ebe 100644
--- a/js/ui/userWidget.js
+++ b/js/ui/userWidget.js
@@ -3,18 +3,32 @@
 // A widget showing the user avatar and name
 /* exported UserWidget */
 
-const { Clutter, GLib, GObject, St } = imports.gi;
+import Clutter from 'gi://Clutter';
+import GLib from 'gi://GLib';
+import GObject from 'gi://GObject';
+import St from 'gi://St';
 
-
-var AVATAR_ICON_SIZE = 64;
+export let AVATAR_ICON_SIZE = 64;
 
 // Adapted from gdm/gui/user-switch-applet/applet.c
 //
 // Copyright (C) 2004-2005 James M. Cape <jcape ignore-your tv>.
 // Copyright (C) 2008,2009 Red Hat, Inc.
 
-var Avatar = GObject.registerClass(
+/** 
+ * @typedef {object} AvatarProps: An object with the properties:
+ * @property {string} [styleClass]
+ * @property {boolean} [reactive]
+ * @property {number} [iconSize]
+ */
+
+export const Avatar = GObject.registerClass(
 class Avatar extends St.Bin {
+    /**
+     * _init:
+     * @param {import('gi://AccountsService').User} user
+     * @param {Partial<AvatarProps>} [params]
+     */
     _init(user, params = {}) {
         let themeContext = St.ThemeContext.get_for_stage(global.stage);
         const {
@@ -103,8 +117,11 @@ class Avatar extends St.Bin {
     }
 });
 
-var UserWidgetLabel = GObject.registerClass(
+export const UserWidgetLabel = GObject.registerClass(
 class UserWidgetLabel extends St.Widget {
+    /**
+     * @param {*} user 
+     */
     _init(user) {
         super._init({ layout_manager: new Clutter.BinLayout() });
 
@@ -186,8 +203,12 @@ class UserWidgetLabel extends St.Widget {
     }
 });
 
-var UserWidget = GObject.registerClass(
+export const UserWidget = GObject.registerClass(
 class UserWidget extends St.BoxLayout {
+    /**
+     * @param {*} user 
+     * @param {*} orientation 
+     */
     _init(user, orientation = Clutter.Orientation.HORIZONTAL) {
         // If user is null, that implies a username-based login authorization.
         this._user = user;
diff --git a/js/ui/welcomeDialog.js b/js/ui/welcomeDialog.js
index cf6540fe23..24bc9958b0 100644
--- a/js/ui/welcomeDialog.js
+++ b/js/ui/welcomeDialog.js
@@ -1,19 +1,20 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported WelcomeDialog */
 
-const { Clutter, GObject, Shell, St } = imports.gi;
-
+import Clutter from 'gi://Clutter';
+import GObject from 'gi://GObject';
+import Shell from 'gi://Shell';
+import St from 'gi://St';
+import * as ModalDialog from './modalDialog.js';
+import Main from './main.js';
 const Config = imports.misc.config;
-const Dialog = imports.ui.dialog;
-const Main = imports.ui.main;
-const ModalDialog = imports.ui.modalDialog;
-
+import * as Dialog from './dialog.js';
 var DialogResponse = {
     NO_THANKS: 0,
     TAKE_TOUR: 1,
 };
 
-var WelcomeDialog = GObject.registerClass(
+export const WelcomeDialog = GObject.registerClass(
 class WelcomeDialog extends ModalDialog.ModalDialog {
     _init() {
         super._init({ styleClass: 'welcome-dialog' });
@@ -26,9 +27,9 @@ class WelcomeDialog extends ModalDialog.ModalDialog {
 
     open() {
         if (!this._tourAppInfo)
-            return;
+            return false;
 
-        super.open();
+        return super.open();
     }
 
     _buildLayout() {
diff --git a/js/ui/windowAttentionHandler.js b/js/ui/windowAttentionHandler.js
index 346fad88d1..ea856d7032 100644
--- a/js/ui/windowAttentionHandler.js
+++ b/js/ui/windowAttentionHandler.js
@@ -1,12 +1,14 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported WindowAttentionHandler */
 
-const { GObject, Shell } = imports.gi;
+import GObject from 'gi://GObject';
+import Shell from 'gi://Shell';
 
-const Main = imports.ui.main;
-const MessageTray = imports.ui.messageTray;
 
-var WindowAttentionHandler = class {
+import Main from './main.js';
+import * as MessageTray from './messageTray.js';
+
+export class WindowAttentionHandler {
     constructor() {
         this._tracker = Shell.WindowTracker.get_default();
         this._windowDemandsAttentionId = global.display.connect('window-demands-attention',
@@ -34,7 +36,7 @@ var WindowAttentionHandler = class {
             return;
 
         let app = this._tracker.get_window_app(window);
-        let source = new WindowAttentionSource(app, window);
+        let source = new WindowAttentionSource({ app, window });
         Main.messageTray.add(source);
 
         let [title, banner] = this._getTitleAndBanner(app, window);
@@ -54,13 +56,24 @@ var WindowAttentionHandler = class {
     }
 };
 
-var WindowAttentionSource = GObject.registerClass(
+/**
+ * @typedef {object} WindowAttentionSourceParams
+ * @property {Shell.App} app
+ * @property {import('gi://Meta').Window} window
+ */
+
+export const WindowAttentionSource = GObject.registerClass(
 class WindowAttentionSource extends MessageTray.Source {
-    _init(app, window) {
+    /**
+     * @param {import('./messageTray.js').SourceParams & WindowAttentionSourceParams} params
+     */
+    _init(params) {
+        const { window, app } = params;
+
         this._window = window;
         this._app = app;
 
-        super._init(app.get_name());
+        super._init({ title: app.get_name() });
 
         this.signalIDs = [];
         this.signalIDs.push(this._window.connect('notify::demands-attention',
diff --git a/js/ui/windowManager.js b/js/ui/windowManager.js
index 2e3e125b68..a1ec2fb40f 100644
--- a/js/ui/windowManager.js
+++ b/js/ui/windowManager.js
@@ -1,39 +1,66 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported WindowManager */
 
-const { Clutter, Gio, GLib, GObject, Meta, Shell, St } = imports.gi;
-
-const AltTab = imports.ui.altTab;
-const AppFavorites = imports.ui.appFavorites;
-const Dialog = imports.ui.dialog;
-const WorkspaceSwitcherPopup = imports.ui.workspaceSwitcherPopup;
-const InhibitShortcutsDialog = imports.ui.inhibitShortcutsDialog;
-const Main = imports.ui.main;
-const ModalDialog = imports.ui.modalDialog;
-const WindowMenu = imports.ui.windowMenu;
-const PadOsd = imports.ui.padOsd;
-const EdgeDragAction = imports.ui.edgeDragAction;
-const CloseDialog = imports.ui.closeDialog;
-const SwitchMonitor = imports.ui.switchMonitor;
-const IBusManager = imports.misc.ibusManager;
-const WorkspaceAnimation = imports.ui.workspaceAnimation;
-
-const { loadInterfaceXML } = imports.misc.fileUtils;
-
-var SHELL_KEYBINDINGS_SCHEMA = 'org.gnome.shell.keybindings';
-var MINIMIZE_WINDOW_ANIMATION_TIME = 200;
-var SHOW_WINDOW_ANIMATION_TIME = 150;
-var DIALOG_SHOW_WINDOW_ANIMATION_TIME = 100;
-var DESTROY_WINDOW_ANIMATION_TIME = 150;
-var DIALOG_DESTROY_WINDOW_ANIMATION_TIME = 100;
-var WINDOW_ANIMATION_TIME = 250;
-var SCROLL_TIMEOUT_TIME = 150;
-var DIM_BRIGHTNESS = -0.3;
-var DIM_TIME = 500;
-var UNDIM_TIME = 250;
-var APP_MOTION_THRESHOLD = 30;
-
-var ONE_SECOND = 1000; // in ms
+import Clutter from 'gi://Clutter';
+import Gio from 'gi://Gio';
+import GLib from 'gi://GLib';
+import GObject from 'gi://GObject';
+import Meta from 'gi://Meta';
+import Shell from 'gi://Shell';
+import St from 'gi://St';
+
+
+import * as AltTab from './altTab.js';
+import * as AppFavorites from './appFavorites.js';
+import * as Dialog from './dialog.js';
+import * as WorkspaceSwitcherPopup from './workspaceSwitcherPopup.js';
+import * as InhibitShortcutsDialog from './inhibitShortcutsDialog.js';
+import Main from './main.js';
+import * as ModalDialog from './modalDialog.js';
+import * as WindowMenu from './windowMenu.js';
+import * as PadOsd from './padOsd.js';
+import * as EdgeDragAction from './edgeDragAction.js';
+import * as CloseDialog from './closeDialog.js';
+import * as SwipeTracker from './swipeTracker.js';
+import * as SwitchMonitor from './switchMonitor.js';
+import * as IBusManager from '../misc/ibusManager.js';
+import * as WorkspaceAnimation from './workspaceAnimation.js';
+
+import { loadInterfaceXML } from '../misc/fileUtilsModule.js';
+
+export const SHELL_KEYBINDINGS_SCHEMA = 'org.gnome.shell.keybindings';
+export const MINIMIZE_WINDOW_ANIMATION_TIME = 200;
+export const SHOW_WINDOW_ANIMATION_TIME = 150;
+export const DIALOG_SHOW_WINDOW_ANIMATION_TIME = 100;
+export const DESTROY_WINDOW_ANIMATION_TIME = 150;
+export const DIALOG_DESTROY_WINDOW_ANIMATION_TIME = 100;
+export const WINDOW_ANIMATION_TIME = 250;
+export const SCROLL_TIMEOUT_TIME = 150;
+export const DIM_BRIGHTNESS = -0.3;
+export const DIM_TIME = 500;
+export const UNDIM_TIME = 250;
+export const APP_MOTION_THRESHOLD = 30;
+
+export const ONE_SECOND = 1000; // in ms
+
+const MOTION_DIRECTIONS = [
+    Meta.MotionDirection.UP,
+    Meta.MotionDirection.DOWN,
+    Meta.MotionDirection.LEFT,
+    Meta.MotionDirection.RIGHT,
+    Meta.MotionDirection.UP_LEFT,
+    Meta.MotionDirection.UP_RIGHT,
+    Meta.MotionDirection.DOWN_LEFT,
+    Meta.MotionDirection.DOWN_RIGHT,
+];
+
+/*
+ * When the last window closed on a workspace is a dialog or splash
+ * screen, we assume that it might be an initial window shown before
+ * the main window of an application, and give the app a grace period
+ * where it can map another window before we remove the workspace.
+ */
+export let LAST_WINDOW_GRACE_TIME = 1000;
 
 var MIN_NUM_WORKSPACES = 2;
 
@@ -50,8 +77,12 @@ Gio._promisify(Shell,
 Gio._promisify(Shell,
     'util_stop_systemd_unit', 'util_stop_systemd_unit_finish');
 
-var DisplayChangeDialog = GObject.registerClass(
+export const DisplayChangeDialog = GObject.registerClass(
 class DisplayChangeDialog extends ModalDialog.ModalDialog {
+    /**
+     * 
+     * @param {Shell.WM | any} wm 
+     */
     _init(wm) {
         super._init();
 
@@ -122,7 +153,7 @@ class DisplayChangeDialog extends ModalDialog.ModalDialog {
     }
 });
 
-var WindowDimmer = GObject.registerClass(
+export const WindowDimmer = GObject.registerClass(
 class WindowDimmer extends Clutter.BrightnessContrastEffect {
     _init() {
         super._init({
@@ -159,7 +190,7 @@ class WindowDimmer extends Clutter.BrightnessContrastEffect {
     }
 });
 
-function getWindowDimmer(actor) {
+export function getWindowDimmer(actor) {
     let enabled = Meta.prefs_get_attach_modal_dialogs();
     let effect = actor.get_effect(WINDOW_DIMMER_EFFECT_NAME);
 
@@ -172,15 +203,8 @@ function getWindowDimmer(actor) {
     return effect;
 }
 
-/*
- * When the last window closed on a workspace is a dialog or splash
- * screen, we assume that it might be an initial window shown before
- * the main window of an application, and give the app a grace period
- * where it can map another window before we remove the workspace.
- */
-var LAST_WINDOW_GRACE_TIME = 1000;
 
-var WorkspaceTracker = class {
+export class WorkspaceTracker {
     constructor(wm) {
         this._wm = wm;
 
@@ -385,7 +409,7 @@ var WorkspaceTracker = class {
     }
 };
 
-var TilePreview = GObject.registerClass(
+export const TilePreview = GObject.registerClass(
 class TilePreview extends St.Widget {
     _init() {
         super._init();
@@ -471,7 +495,7 @@ class TilePreview extends St.Widget {
     }
 });
 
-var AppSwitchAction = GObject.registerClass({
+export const AppSwitchAction = GObject.registerClass({
     Signals: { 'activated': {} },
 }, class AppSwitchAction extends Clutter.GestureAction {
     _init() {
@@ -532,7 +556,7 @@ var AppSwitchAction = GObject.registerClass({
     }
 });
 
-var ResizePopup = GObject.registerClass(
+export const ResizePopup = GObject.registerClass(
 class ResizePopup extends St.Widget {
     _init() {
         super._init({ layout_manager: new Clutter.BinLayout() });
@@ -555,7 +579,7 @@ class ResizePopup extends St.Widget {
     }
 });
 
-var WindowManager = class {
+export class WindowManager {
     constructor() {
         this._shellwm =  global.window_manager;
 
@@ -1398,7 +1422,7 @@ var WindowManager = class {
     }
 
     _hasAttachedDialogs(window, ignoreWindow) {
-        var count = 0;
+        let count = 0;
         window.foreach_transient(win => {
             if (win != ignoreWindow &&
                 win.is_attached_dialog() &&
diff --git a/js/ui/windowMenu.js b/js/ui/windowMenu.js
index 27cecdac23..779d8a7d04 100644
--- a/js/ui/windowMenu.js
+++ b/js/ui/windowMenu.js
@@ -1,13 +1,16 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*
 /* exported WindowMenuManager */
 
-const { GLib, Meta, St } = imports.gi;
+import GLib from 'gi://GLib';
+import Meta from 'gi://Meta';
+import St from 'gi://St';
 
-const BoxPointer = imports.ui.boxpointer;
-const Main = imports.ui.main;
-const PopupMenu = imports.ui.popupMenu;
 
-var WindowMenu = class extends PopupMenu.PopupMenu {
+import * as BoxPointer from './boxpointer.js';
+import Main from './main.js';
+import * as PopupMenu from './popupMenu.js';
+
+export class WindowMenu extends PopupMenu.PopupMenu {
     constructor(window, sourceActor) {
         super(sourceActor, 0, St.Side.TOP);
 
@@ -192,7 +195,7 @@ var WindowMenu = class extends PopupMenu.PopupMenu {
     }
 };
 
-var WindowMenuManager = class {
+export class WindowMenuManager {
     constructor() {
         this._manager = new PopupMenu.PopupMenuManager(Main.layoutManager.dummyCursor);
 
diff --git a/js/ui/windowPreview.js b/js/ui/windowPreview.js
index e67ec9ec0f..d9f82c6704 100644
--- a/js/ui/windowPreview.js
+++ b/js/ui/windowPreview.js
@@ -1,28 +1,35 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported WindowPreview */
 
-const { Atk, Clutter, GLib, GObject,
-        Graphene, Meta, Pango, Shell, St } = imports.gi;
+import Atk from 'gi://Atk';
+import Clutter from 'gi://Clutter';
+import GLib from 'gi://GLib';
+import GObject from 'gi://GObject';
+import Graphene from 'gi://Graphene';
+import Meta from 'gi://Meta';
+import Pango from 'gi://Pango';
+import Shell from 'gi://Shell';
+import St from 'gi://St';
 
-const DND = imports.ui.dnd;
-const OverviewControls = imports.ui.overviewControls;
+import * as DND from './dnd.js';
+import * as OverviewControls from './overviewControls.js';
 
-var WINDOW_DND_SIZE = 256;
+export let WINDOW_DND_SIZE = 256;
 
-var WINDOW_OVERLAY_IDLE_HIDE_TIMEOUT = 750;
-var WINDOW_OVERLAY_FADE_TIME = 200;
+export let WINDOW_OVERLAY_IDLE_HIDE_TIMEOUT = 750;
+export let WINDOW_OVERLAY_FADE_TIME = 200;
 
-var WINDOW_SCALE_TIME = 200;
-var WINDOW_ACTIVE_SIZE_INC = 5; // in each direction
+export let WINDOW_SCALE_TIME = 200;
+export let WINDOW_ACTIVE_SIZE_INC = 5; // in each direction
 
-var DRAGGING_WINDOW_OPACITY = 100;
+export let DRAGGING_WINDOW_OPACITY = 100;
 
 const ICON_SIZE = 64;
 const ICON_OVERLAP = 0.7;
 
 const ICON_TITLE_SPACING = 6;
 
-var WindowPreview = GObject.registerClass({
+export const WindowPreview = GObject.registerClass({
     Properties: {
         'overlay-enabled': GObject.ParamSpec.boolean(
             'overlay-enabled', 'overlay-enabled', 'overlay-enabled',
@@ -38,6 +45,17 @@ var WindowPreview = GObject.registerClass({
         'size-changed': {},
     },
 }, class WindowPreview extends Shell.WindowPreview {
+    /** @returns {Clutter.Actor<Shell.WindowPreviewLayout>} */
+    get window_container() {
+        // Cast the window_container to the appropriate type...
+
+        return  /** @type {Clutter.Actor<Shell.WindowPreviewLayout>} */ (super.window_container);
+    }
+    
+    set window_container(window_container) {
+        super.window_container = window_container;
+    }
+
     _init(metaWindow, workspace, overviewAdjustment) {
         this.metaWindow = metaWindow;
         this.metaWindow._delegate = this;
@@ -54,6 +72,7 @@ var WindowPreview = GObject.registerClass({
 
         const windowContainer = new Clutter.Actor({
             pivot_point: new Graphene.Point({ x: 0.5, y: 0.5 }),
+            layout_manager: /** @type {Shell.WindowPreviewLayout} */ (null),
         });
         this.window_container = windowContainer;
 
@@ -628,6 +647,8 @@ var WindowPreview = GObject.registerClass({
                 let [x, y] = action.get_coords();
                 action.release();
                 this._draggable.startDrag(x, y, global.get_current_time(), this._dragTouchSequence, 
event.get_device());
+                
+                return false;
             });
         } else {
             this.showOverlay(true);
diff --git a/js/ui/workspace.js b/js/ui/workspace.js
index 1c3a15710a..71cbf1a9cd 100644
--- a/js/ui/workspace.js
+++ b/js/ui/workspace.js
@@ -1,26 +1,31 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported Workspace */
 
-const { Clutter, GLib, GObject, Graphene, Meta, St } = imports.gi;
+import Clutter from 'gi://Clutter';
+import GLib from 'gi://GLib';
+import GObject from 'gi://GObject';
+import St from 'gi://St';
+import Graphene from 'gi://Graphene';
 
-const Background = imports.ui.background;
-const DND = imports.ui.dnd;
-const Main = imports.ui.main;
-const OverviewControls = imports.ui.overviewControls;
-const Util = imports.misc.util;
-const { WindowPreview } = imports.ui.windowPreview;
+import * as Background from './background.js';
+import * as DND from './dnd.js';
+import Main from './main.js';
+import Meta from 'gi://Meta';
+import * as OverviewControls from './overviewControls.js';
+import * as Util from '../misc/util.js';
+import { WindowPreview } from './windowPreview.js';
 
-var WINDOW_PREVIEW_MAXIMUM_SCALE = 0.95;
+export let WINDOW_PREVIEW_MAXIMUM_SCALE = 0.95;
 
-var WINDOW_REPOSITIONING_DELAY = 750;
+export let WINDOW_REPOSITIONING_DELAY = 750;
 
 // When calculating a layout, we calculate the scale of windows and the percent
 // of the available area the new layout uses. If the values for the new layout,
 // when weighted with the values as below, are worse than the previous layout's,
 // we stop looking for a new layout and use the previous layout.
 // Otherwise, we keep looking for a new layout.
-var LAYOUT_SCALE_WEIGHT = 1;
-var LAYOUT_SPACE_WEIGHT = 0.1;
+export let LAYOUT_SCALE_WEIGHT = 1;
+export let LAYOUT_SPACE_WEIGHT = 0.1;
 
 const BACKGROUND_CORNER_RADIUS_PIXELS = 30;
 const BACKGROUND_MARGIN = 12;
@@ -95,7 +100,7 @@ const BACKGROUND_MARGIN = 12;
 // each window's "cell" area to be the same, but we shrink the thumbnail
 // and center it horizontally, and align it to the bottom vertically.
 
-var LayoutStrategy = class {
+export class LayoutStrategy {
     constructor(params = {}) {
         const {
             monitor = null,
@@ -388,7 +393,7 @@ function animateAllocation(actor, box) {
     return actor.get_transition('allocation');
 }
 
-var WorkspaceLayout = GObject.registerClass({
+export const WorkspaceLayout = GObject.registerClass({
     Properties: {
         'spacing': GObject.ParamSpec.double(
             'spacing', 'Spacing', 'Spacing',
@@ -572,8 +577,9 @@ var WorkspaceLayout = GObject.registerClass({
 
     _syncWorkareaTracking() {
         if (this._container) {
-            if (this._workAreaChangedId)
-                return;
+            // FIXME
+            // if (this._workAreaChangedId)
+            //     return;
             this._workarea = Main.layoutManager.getWorkAreaForMonitor(this._monitorIndex);
             this._workareasChangedId =
                 global.display.connect('workareas-changed', () => {
@@ -592,6 +598,9 @@ var WorkspaceLayout = GObject.registerClass({
         this._stateAdjustment.actor = container;
     }
 
+    /**
+     * @returns {[number, number]}
+     */
     vfunc_get_preferred_width(container, forHeight) {
         const workarea = this._getAdjustedWorkarea(container);
         if (forHeight === -1)
@@ -603,6 +612,9 @@ var WorkspaceLayout = GObject.registerClass({
         return [0, widthPreservingAspectRatio];
     }
 
+    /**
+     * @returns {[number, number]}
+     */
     vfunc_get_preferred_height(container, forWidth) {
         const workarea = this._getAdjustedWorkarea(container);
         if (forWidth === -1)
@@ -765,7 +777,7 @@ var WorkspaceLayout = GObject.registerClass({
 
     /**
      * addWindow:
-     * @param {WindowPreview} window: the window to add
+     * @param {WindowPreview["prototype"]} window: the window to add
      * @param {Meta.Window} metaWindow: the MetaWindow of the window
      *
      * Adds @window to the workspace, it will be shown immediately if
@@ -806,7 +818,7 @@ var WorkspaceLayout = GObject.registerClass({
 
     /**
      * removeWindow:
-     * @param {WindowPreview} window: the window to remove
+     * @param {WindowPreview["prototype"]} window: the window to remove
      *
      * Removes @window from the workspace if @window is a part of the
      * workspace. If the layout-frozen property is set to true, the
@@ -1055,7 +1067,8 @@ class WorkspaceBackground extends St.Widget {
 /**
  * @metaWorkspace: a #Meta.Workspace, or null
  */
-var Workspace = GObject.registerClass(
+export const Workspace = GObject.registerClass(
+/** @extends {St.Widget<typeof WorkspaceLayout["prototype"]>} */
 class Workspace extends St.Widget {
     _init(metaWorkspace, monitorIndex, overviewAdjustment) {
         super._init({
@@ -1077,6 +1090,7 @@ class Workspace extends St.Widget {
             reactive: true,
             x_expand: true,
             y_expand: true,
+            layout_manager: /** @type {WorkspaceLayout["prototype"]} */ (null),
         });
         this._container.layout_manager = layoutManager;
         this.add_child(this._container);
@@ -1216,6 +1230,9 @@ class Workspace extends St.Widget {
             '[gnome-shell] this._layoutFrozenId');
     }
 
+    /**
+     * @param {Meta.Window} metaWin 
+     */
     _doAddWindow(metaWin) {
         let win = metaWin.get_compositor_private();
 
diff --git a/js/ui/workspaceAnimation.js b/js/ui/workspaceAnimation.js
index d240fe1562..5bd8db359a 100644
--- a/js/ui/workspaceAnimation.js
+++ b/js/ui/workspaceAnimation.js
@@ -1,12 +1,17 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported WorkspaceAnimationController */
 
-const { Clutter, GObject, Meta, Shell, St } = imports.gi;
+import Clutter from 'gi://Clutter';
+import GObject from 'gi://GObject';
+import Meta from 'gi://Meta';
+import Shell from 'gi://Shell';
+import St from 'gi://St';
 
-const Background = imports.ui.background;
-const Layout = imports.ui.layout;
-const Main = imports.ui.main;
-const SwipeTracker = imports.ui.swipeTracker;
+import * as Background from './background.js';
+import * as Layout from './layout.js';
+import * as SwipeTracker from './swipeTracker.js';
+
+import Main from './main.js';
 
 const WINDOW_ANIMATION_TIME = 250;
 const WORKSPACE_SPACING = 100;
@@ -267,7 +272,7 @@ const MonitorGroup = GObject.registerClass({
     }
 });
 
-var WorkspaceAnimationController = class {
+export class WorkspaceAnimationController {
     constructor() {
         this._movingWindow = null;
         this._switchData = null;
diff --git a/js/ui/workspaceSwitcherPopup.js b/js/ui/workspaceSwitcherPopup.js
index 5b65bec8ad..a85fb4a35d 100644
--- a/js/ui/workspaceSwitcherPopup.js
+++ b/js/ui/workspaceSwitcherPopup.js
@@ -1,14 +1,19 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported WorkspaceSwitcherPopup */
 
-const { Clutter, GLib, GObject, Meta, St } = imports.gi;
+import Clutter from 'gi://Clutter';
+import GLib from 'gi://GLib';
+import GObject from 'gi://GObject';
+import Meta from 'gi://Meta';
+import St from 'gi://St';
 
-const Main = imports.ui.main;
 
-var ANIMATION_TIME = 100;
-var DISPLAY_TIMEOUT = 600;
+import Main from './main.js';
 
-var WorkspaceSwitcherPopupList = GObject.registerClass(
+ export let  ANIMATION_TIME = 100;
+ export let  DISPLAY_TIMEOUT = 600;
+
+export const WorkspaceSwitcherPopupList = GObject.registerClass(
 class WorkspaceSwitcherPopupList extends St.Widget {
     _init() {
         super._init({
@@ -63,6 +68,9 @@ class WorkspaceSwitcherPopupList extends St.Widget {
         }
     }
 
+    /**
+     * @return {[number, number]}
+     */
     _getSizeForOppositeOrientation() {
         let workArea = Main.layoutManager.getWorkAreaForMonitor(Main.layoutManager.primaryIndex);
 
@@ -119,7 +127,7 @@ class WorkspaceSwitcherPopupList extends St.Widget {
     }
 });
 
-var WorkspaceSwitcherPopup = GObject.registerClass(
+export const WorkspaceSwitcherPopup = GObject.registerClass(
 class WorkspaceSwitcherPopup extends St.Widget {
     _init() {
         super._init({ x: 0,
diff --git a/js/ui/workspaceThumbnail.js b/js/ui/workspaceThumbnail.js
index 412527c938..c18cb5b67d 100644
--- a/js/ui/workspaceThumbnail.js
+++ b/js/ui/workspaceThumbnail.js
@@ -1,50 +1,69 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported WorkspaceThumbnail, ThumbnailsBox */
 
-const { Clutter, Gio, GLib, GObject, Graphene, Meta, Shell, St } = imports.gi;
+import Clutter from 'gi://Clutter';
+import Gio from 'gi://Gio';
+import GLib from 'gi://GLib';
+import GObject from 'gi://GObject';
+import Meta from 'gi://Meta';
+import Shell from 'gi://Shell';
+import St from 'gi://St';
+import Graphene from 'gi://Graphene';
 
-const DND = imports.ui.dnd;
-const Main = imports.ui.main;
-const Util = imports.misc.util;
-const Workspace = imports.ui.workspace;
+
+
+import * as DND from './dnd.js';
+import Main from './main.js';
+import * as WindowPreview from './windowPreview.js';
+import * as Util from '../misc/util.js'
 
 const NUM_WORKSPACES_THRESHOLD = 2;
 
 // The maximum size of a thumbnail is 5% the width and height of the screen
-var MAX_THUMBNAIL_SCALE = 0.05;
+export const MAX_THUMBNAIL_SCALE = 0.05;
 
-var RESCALE_ANIMATION_TIME = 200;
-var SLIDE_ANIMATION_TIME = 200;
+
+export let RESCALE_ANIMATION_TIME = 200;
+export let  SLIDE_ANIMATION_TIME = 200;
 
 // When we create workspaces by dragging, we add a "cut" into the top and
 // bottom of each workspace so that the user doesn't have to hit the
 // placeholder exactly.
-var WORKSPACE_CUT_SIZE = 10;
+export let  WORKSPACE_CUT_SIZE = 10;
 
-var WORKSPACE_KEEP_ALIVE_TIME = 100;
+export let  WORKSPACE_KEEP_ALIVE_TIME = 100;
 
-var MUTTER_SCHEMA = 'org.gnome.mutter';
+const MUTTER_SCHEMA = 'org.gnome.mutter';
 
 /* A layout manager that requests size only for primary_actor, but then allocates
    all using a fixed layout */
-var PrimaryActorLayout = GObject.registerClass(
+export const PrimaryActorLayout = GObject.registerClass(
 class PrimaryActorLayout extends Clutter.FixedLayout {
+    /**
+     * @param {Clutter.Actor} primaryActor 
+     */
     _init(primaryActor) {
         super._init();
 
         this.primaryActor = primaryActor;
     }
 
-    vfunc_get_preferred_width(container, forHeight) {
+    /**
+     * @param {number} forHeight 
+     */
+    vfunc_get_preferred_width(_, forHeight) {
         return this.primaryActor.get_preferred_width(forHeight);
     }
 
-    vfunc_get_preferred_height(container, forWidth) {
+    /**
+     * @param {number} forWidth 
+     */
+    vfunc_get_preferred_height(_, forWidth) {
         return this.primaryActor.get_preferred_height(forWidth);
     }
 });
 
-var WindowClone = GObject.registerClass({
+export const WindowClone = GObject.registerClass({
     Signals: {
         'drag-begin': {},
         'drag-cancelled': {},
@@ -52,6 +71,9 @@ var WindowClone = GObject.registerClass({
         'selected': { param_types: [GObject.TYPE_UINT] },
     },
 }, class WindowClone extends Clutter.Actor {
+    /**
+     * @param {Meta.WindowActor} realWindow 
+     */
     _init(realWindow) {
         let clone = new Clutter.Clone({ source: realWindow });
         super._init({
@@ -78,14 +100,15 @@ var WindowClone = GObject.registerClass({
 
         this._draggable = DND.makeDraggable(this,
                                             { restoreOnSuccess: true,
-                                              dragActorMaxSize: Workspace.WINDOW_DND_SIZE,
-                                              dragActorOpacity: Workspace.DRAGGING_WINDOW_OPACITY });
+                                              dragActorMaxSize: WindowPreview.WINDOW_DND_SIZE,
+                                              dragActorOpacity: WindowPreview.DRAGGING_WINDOW_OPACITY });
         this._draggable.connect('drag-begin', this._onDragBegin.bind(this));
         this._draggable.connect('drag-cancelled', this._onDragCancelled.bind(this));
         this._draggable.connect('drag-end', this._onDragEnd.bind(this));
         this.inDrag = false;
 
         let iter = win => {
+            /** @type {Meta.Window} */
             let actor = win.get_compositor_private();
 
             if (!actor)
@@ -163,7 +186,8 @@ var WindowClone = GObject.registerClass({
     }
 
     _disconnectSignals() {
-        this.get_children().forEach(child => {
+        /** @type {Clutter.Clone[]} */
+        (this.get_children()).forEach(child => {
             let realWindow = child.source;
 
             realWindow.disconnect(child._updateId);
@@ -230,7 +254,7 @@ var WindowClone = GObject.registerClass({
 });
 
 
-var ThumbnailState = {
+export const ThumbnailState = {
     NEW:            0,
     EXPANDING:      1,
     EXPANDED:       2,
@@ -246,7 +270,7 @@ var ThumbnailState = {
 /**
  * @metaWorkspace: a #Meta.Workspace
  */
-var WorkspaceThumbnail = GObject.registerClass({
+export const WorkspaceThumbnail = GObject.registerClass({
     Properties: {
         'collapse-fraction': GObject.ParamSpec.double(
             'collapse-fraction', 'collapse-fraction', 'collapse-fraction',
@@ -607,7 +631,7 @@ var WorkspaceThumbnail = GObject.registerClass({
 });
 
 
-var ThumbnailsBox = GObject.registerClass({
+export const ThumbnailsBox = GObject.registerClass({
     Properties: {
         'expand-fraction': GObject.ParamSpec.double(
             'expand-fraction', 'expand-fraction', 'expand-fraction',
@@ -1258,6 +1282,8 @@ var ThumbnailsBox = GObject.registerClass({
                 },
             });
         });
+
+        return false;
     }
 
     _queueUpdateStates() {
@@ -1408,6 +1434,8 @@ var ThumbnailsBox = GObject.registerClass({
 
             Meta.later_add(Meta.LaterType.BEFORE_REDRAW, () => {
                 this._dropPlaceholder.hide();
+
+                return false;
             });
         }
 
@@ -1438,6 +1466,8 @@ var ThumbnailsBox = GObject.registerClass({
 
                 Meta.later_add(Meta.LaterType.BEFORE_REDRAW, () => {
                     this._dropPlaceholder.show();
+
+                    return false;
                 });
                 x += placeholderWidth + spacing;
             }
diff --git a/js/ui/workspacesView.js b/js/ui/workspacesView.js
index 6dad2df3f5..c2355905d0 100644
--- a/js/ui/workspacesView.js
+++ b/js/ui/workspacesView.js
@@ -1,19 +1,29 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported WorkspacesView, WorkspacesDisplay */
 
-const { Clutter, Gio, GObject, Meta, Shell, St } = imports.gi;
+import Clutter from 'gi://Clutter';
+import Gio from 'gi://Gio';
+import GObject from 'gi://GObject';
+import Meta from 'gi://Meta';
+import Shell from 'gi://Shell';
+import St from 'gi://St';
 
-const Layout = imports.ui.layout;
-const Main = imports.ui.main;
-const OverviewControls = imports.ui.overviewControls;
-const SwipeTracker = imports.ui.swipeTracker;
-const Util = imports.misc.util;
-const Workspace = imports.ui.workspace;
-const { ThumbnailsBox, MAX_THUMBNAIL_SCALE } = imports.ui.workspaceThumbnail;
 
-var WORKSPACE_SWITCH_TIME = 250;
+import { ThumbnailsBox, MAX_THUMBNAIL_SCALE } from './workspaceThumbnail.js';
 
-const MUTTER_SCHEMA = 'org.gnome.mutter';
+import * as Util from '../misc/util.js';
+import * as SwipeTracker from './swipeTracker.js';
+import * as Workspace from './workspace.js';
+import * as OverviewControls from './overviewControls.js';
+import * as Layout from './layout.js';
+
+import Main from './main.js'
+
+import { ANIMATION_TIME } from '../ui/overview.js';
+
+export var WORKSPACE_SWITCH_TIME = 250;
+
+export const MUTTER_SCHEMA = 'org.gnome.mutter';
 
 const WORKSPACE_MIN_SPACING = 24;
 const WORKSPACE_MAX_SPACING = 80;
@@ -81,21 +91,27 @@ var WorkspacesViewBase = GObject.registerClass({
             child.allocate_available_size(0, 0, box.get_width(), box.get_height());
     }
 
+    /**
+     * @returns {[number, number]}
+     */
     vfunc_get_preferred_width() {
         return [0, 0];
     }
 
+    /**
+     * @returns {[number, number]}
+     */
     vfunc_get_preferred_height() {
         return [0, 0];
     }
 });
 
-var FitMode = {
+export const FitMode = {
     SINGLE: 0,
     ALL: 1,
 };
 
-var WorkspacesView = GObject.registerClass(
+export const WorkspacesView = GObject.registerClass(
 class WorkspacesView extends WorkspacesViewBase {
     _init(monitorIndex, controls, scrollAdjustment, fitModeAdjustment, overviewAdjustment) {
         let workspaceManager = global.workspace_manager;
@@ -291,6 +307,9 @@ class WorkspacesView extends WorkspacesViewBase {
         }
     }
 
+    /**
+     * @param {Clutter.ActorBox} box 
+     */
     _getInitialBoxes(box) {
         const offsetBox = new Clutter.ActorBox();
         offsetBox.set_size(...box.get_size());
@@ -546,7 +565,7 @@ class WorkspacesView extends WorkspacesViewBase {
     }
 });
 
-var ExtraWorkspaceView = GObject.registerClass(
+export const ExtraWorkspaceView = GObject.registerClass(
 class ExtraWorkspaceView extends WorkspacesViewBase {
     _init(monitorIndex, overviewAdjustment) {
         super._init(monitorIndex, overviewAdjustment);
@@ -828,7 +847,7 @@ class SecondaryMonitorDisplay extends St.Widget {
     }
 });
 
-var WorkspacesDisplay = GObject.registerClass(
+export const WorkspacesDisplay = GObject.registerClass(
 class WorkspacesDisplay extends St.Widget {
     _init(controls, scrollAdjustment, overviewAdjustment) {
         super._init({
diff --git a/js/ui/xdndHandler.js b/js/ui/xdndHandler.js
index 4154b7f398..d45c9bfcfe 100644
--- a/js/ui/xdndHandler.js
+++ b/js/ui/xdndHandler.js
@@ -1,13 +1,12 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 /* exported XdndHandler */
 
-const { Clutter } = imports.gi;
-const Signals = imports.misc.signals;
+import Clutter from 'gi://Clutter';
+import Main from './main.js';
+import * as Signals from '../misc/signals.js';
+import * as DND from './dnd.js';
 
-const DND = imports.ui.dnd;
-const Main = imports.ui.main;
-
-var XdndHandler = class extends Signals.EventEmitter {
+export class XdndHandler extends Signals.EventEmitter {
     constructor() {
         super();
 
@@ -104,7 +103,7 @@ var XdndHandler = class extends Signals.EventEmitter {
         }
 
         while (pickedActor) {
-            if (pickedActor._delegate && pickedActor._delegate.handleDragOver) {
+            if (pickedActor._delegate && DND.handlesDragOver(pickedActor._delegate)) {
                 let [r_, targX, targY] = pickedActor.transform_stage_point(x, y);
                 let result = pickedActor._delegate.handleDragOver(this,
                                                                   dragEvent.dragActor,
diff --git a/src/main.c b/src/main.c
index 3cd9e10a5b..9a42461529 100644
--- a/src/main.c
+++ b/src/main.c
@@ -393,8 +393,8 @@ list_modes (const char  *option_name,
 
   shell_introspection_init ();
 
-  script = "imports.ui.environment.init();"
-           "imports.ui.sessionMode.listModes();";
+  script = ""
+           "";
   if (!gjs_context_eval (context, script, -1, "<main>", &status, NULL))
       g_message ("Retrieving list of available modes failed.");
 
diff --git a/tests/interactive/background-repeat.js b/tests/interactive/background-repeat.js
index 1377f74249..3179a8a089 100644
--- a/tests/interactive/background-repeat.js
+++ b/tests/interactive/background-repeat.js
@@ -2,7 +2,9 @@
 
 const UI = imports.testcommon.ui;
 
-const { Clutter, St } = imports.gi;
+import Clutter from 'gi://Clutter';
+import St from 'gi://St';
+
 
 function test() {
     let stage = new Clutter.Stage({ width: 640, height: 480 });
diff --git a/tests/interactive/background-size.js b/tests/interactive/background-size.js
index 8f8738da91..367f47e116 100644
--- a/tests/interactive/background-size.js
+++ b/tests/interactive/background-size.js
@@ -2,7 +2,11 @@
 
 const UI = imports.testcommon.ui;
 
-const { Cogl, Clutter, Meta, St } = imports.gi;
+import Cogl from 'gi://Cogl';
+import Clutter from 'gi://Clutter';
+import Meta from 'gi://Meta';
+import St from 'gi://St';
+
 
 
 function test() {
diff --git a/tests/interactive/border-radius.js b/tests/interactive/border-radius.js
index 4d26518bdc..a5eb6a3a7b 100644
--- a/tests/interactive/border-radius.js
+++ b/tests/interactive/border-radius.js
@@ -2,7 +2,9 @@
 
 const UI = imports.testcommon.ui;
 
-const { Clutter, St } = imports.gi;
+import Clutter from 'gi://Clutter';
+import St from 'gi://St';
+
 
 function test() {
     let stage = new Clutter.Stage({ width: 640, height: 480 });
diff --git a/tests/interactive/border-width.js b/tests/interactive/border-width.js
index 30c7575a01..4d71d443f3 100644
--- a/tests/interactive/border-width.js
+++ b/tests/interactive/border-width.js
@@ -2,7 +2,9 @@
 
 const UI = imports.testcommon.ui;
 
-const { Clutter, St } = imports.gi;
+import Clutter from 'gi://Clutter';
+import St from 'gi://St';
+
 
 function test() {
     let stage = new Clutter.Stage({ width: 640, height: 480 });
diff --git a/tests/interactive/borders.js b/tests/interactive/borders.js
index 4812acbfa9..aa76720bba 100644
--- a/tests/interactive/borders.js
+++ b/tests/interactive/borders.js
@@ -2,7 +2,8 @@
 
 const UI = imports.testcommon.ui;
 
-const { Clutter, St } = imports.gi;
+import Clutter from 'gi://Clutter';
+import St from 'gi://St';
 
 function test() {
     let stage = new Clutter.Stage({ width: 640, height: 480 });
diff --git a/tests/interactive/box-layout.js b/tests/interactive/box-layout.js
index bb9a5bbc2a..ec1550b701 100644
--- a/tests/interactive/box-layout.js
+++ b/tests/interactive/box-layout.js
@@ -2,7 +2,9 @@
 
 const UI = imports.testcommon.ui;
 
-const { Clutter, St } = imports.gi;
+import Clutter from 'gi://Clutter';
+import St from 'gi://St';
+
 
 function test() {
     let stage = new Clutter.Stage();
diff --git a/tests/interactive/box-shadow-animated.js b/tests/interactive/box-shadow-animated.js
index cf117a7f3a..4d5753029d 100644
--- a/tests/interactive/box-shadow-animated.js
+++ b/tests/interactive/box-shadow-animated.js
@@ -2,7 +2,10 @@
 
 const UI = imports.testcommon.ui;
 
-const { Clutter, GLib, St } = imports.gi;
+import Clutter from 'gi://Clutter';
+import GLib from 'gi://GLib';
+import St from 'gi://St';
+
 
 const DELAY = 2000;
 
@@ -22,7 +25,7 @@ function resize_animated(label) {
     }
 }
 
-function get_css_style(shadow_style)
+export function get_css_style(shadow_style)
 {
     return 'border: 20px solid black;' +
         'border-radius: 20px;' +
diff --git a/tests/interactive/box-shadows.js b/tests/interactive/box-shadows.js
index c9c677c97f..077cf18dd9 100644
--- a/tests/interactive/box-shadows.js
+++ b/tests/interactive/box-shadows.js
@@ -2,7 +2,8 @@
 
 const UI = imports.testcommon.ui;
 
-const { Clutter, St } = imports.gi;
+import Clutter from 'gi://Clutter';
+import St from 'gi://St';
 
 function test() {
     let stage = new Clutter.Stage({ width: 640, height: 480 });
diff --git a/tests/interactive/calendar.js b/tests/interactive/calendar.js
index d1d435a528..6098ecbb7a 100644
--- a/tests/interactive/calendar.js
+++ b/tests/interactive/calendar.js
@@ -2,7 +2,9 @@
 
 const UI = imports.testcommon.ui;
 
-const { Clutter, St } = imports.gi;
+import Clutter from 'gi://Clutter';
+import St from 'gi://St';
+
 
 function test() {
     let stage = new Clutter.Stage({ width: 400, height: 400 });
diff --git a/tests/interactive/css-fonts.js b/tests/interactive/css-fonts.js
index a25769318f..c6d581ac18 100644
--- a/tests/interactive/css-fonts.js
+++ b/tests/interactive/css-fonts.js
@@ -2,7 +2,9 @@
 
 const UI = imports.testcommon.ui;
 
-const { Clutter, St } = imports.gi;
+import Clutter from 'gi://Clutter';
+import St from 'gi://St';
+
 
 function test() {
     let stage = new Clutter.Stage();
diff --git a/tests/interactive/entry.js b/tests/interactive/entry.js
index 9ae010604e..0860aa7ce4 100644
--- a/tests/interactive/entry.js
+++ b/tests/interactive/entry.js
@@ -2,7 +2,10 @@
 
 const UI = imports.testcommon.ui;
 
-const { Clutter, GLib, St } = imports.gi;
+import Clutter from 'gi://Clutter';
+import GLib from 'gi://GLib';
+import St from 'gi://St';
+
 
 function test() {
     let stage = new Clutter.Stage({ width: 400, height: 400 });
diff --git a/tests/interactive/gapplication.js b/tests/interactive/gapplication.js
index ec38b806f3..565b726081 100755
--- a/tests/interactive/gapplication.js
+++ b/tests/interactive/gapplication.js
@@ -1,8 +1,14 @@
 #!/usr/bin/env gjs
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 
-imports.gi.versions = { Gdk: '3.0', Gtk: '3.0' };
-const { Gdk, Gio, GLib, Gtk } = imports.gi;
+import 'gi://Gtk?version=3.0';
+import 'gi://Gdk?version=3.0';
+
+import Gdk from 'gi://Gdk';
+import Gio from 'gi://Gio';
+import GLib from 'gi://GLib';
+import Gtk from 'gi://Gtk';
+
 
 function do_action(action, parameter) {
     print ("Action '" + action.name + "' invoked");
diff --git a/tests/interactive/icons.js b/tests/interactive/icons.js
index 65b7f65609..54441d52bc 100644
--- a/tests/interactive/icons.js
+++ b/tests/interactive/icons.js
@@ -2,7 +2,9 @@
 
 const UI = imports.testcommon.ui;
 
-const { Clutter, St } = imports.gi;
+import Clutter from 'gi://Clutter';
+import St from 'gi://St';
+
 
 function test() {
     let stage = new Clutter.Stage();
diff --git a/tests/interactive/inline-style.js b/tests/interactive/inline-style.js
index 3952c3a984..1439ca44c7 100644
--- a/tests/interactive/inline-style.js
+++ b/tests/interactive/inline-style.js
@@ -2,7 +2,9 @@
 
 const UI = imports.testcommon.ui;
 
-const { Clutter, St } = imports.gi;
+import Clutter from 'gi://Clutter';
+import St from 'gi://St';
+
 
 function test() {
     let stage = new Clutter.Stage();
diff --git a/tests/interactive/scroll-view-sizing.js b/tests/interactive/scroll-view-sizing.js
index a6c682e546..0b07414f3f 100644
--- a/tests/interactive/scroll-view-sizing.js
+++ b/tests/interactive/scroll-view-sizing.js
@@ -2,7 +2,12 @@
 
 const UI = imports.testcommon.ui;
 
-const { Clutter, GObject, Gtk, Shell, St } = imports.gi;
+import Clutter from 'gi://Clutter';
+import GObject from 'gi://GObject';
+import Gtk from 'gi://Gtk';
+import Shell from 'gi://Shell';
+import St from 'gi://St';
+
 
 // This is an interactive test of the sizing behavior of StScrollView. It
 // may be interesting in the future to split out the two classes at the
@@ -28,7 +33,7 @@ const BOX_WIDTHS = [
 
 const SPACING = 10;
 
-var FlowedBoxes = GObject.registerClass(
+export const FlowedBoxes = GObject.registerClass(
 class FlowedBoxes extends St.Widget {
     _init() {
         super._init();
@@ -117,7 +122,7 @@ class FlowedBoxes extends St.Widget {
 //
 // This is currently only written for the case where the child is height-for-width
 
-var SizingIllustrator = GObject.registerClass(
+export const SizingIllustrator = GObject.registerClass(
 class SizingIllustrator extends St.Widget {
     _init() {
         super._init();
diff --git a/tests/interactive/scrolling.js b/tests/interactive/scrolling.js
index 91951ce10a..50d1aaa5a6 100644
--- a/tests/interactive/scrolling.js
+++ b/tests/interactive/scrolling.js
@@ -2,7 +2,10 @@
 
 const UI = imports.testcommon.ui;
 
-const { Clutter, Gtk, St } = imports.gi;
+import Clutter from 'gi://Clutter';
+import Gtk from 'gi://Gtk';
+import St from 'gi://St';
+
 
 function test() {
     let stage = new Clutter.Stage();
diff --git a/tests/interactive/test-title.js b/tests/interactive/test-title.js
index 0a468ddd50..09e2c62ac9 100755
--- a/tests/interactive/test-title.js
+++ b/tests/interactive/test-title.js
@@ -1,8 +1,9 @@
 #!/usr/bin/env gjs
 
-imports.gi.versions.Gtk = '3.0';
+import 'gi://Gtk?version=3.0';
 
-const { GLib, Gtk } = imports.gi;
+import GLib from 'gi://GLib';
+import Gtk from 'gi://Gtk';
 
 function nextTitle() {
     let length = Math.random() * 20;
diff --git a/tests/interactive/transitions.js b/tests/interactive/transitions.js
index 7b2eac1c73..a9e39ecfc2 100644
--- a/tests/interactive/transitions.js
+++ b/tests/interactive/transitions.js
@@ -2,7 +2,8 @@
 
 const UI = imports.testcommon.ui;
 
-const { Clutter, St } = imports.gi;
+import Clutter from 'gi://Clutter';
+import St from 'gi://St';
 
 function test() {
     let stage = new Clutter.Stage();
diff --git a/tests/testcommon/ui.js b/tests/testcommon/ui.js
index abacea5f76..b98fb45675 100644
--- a/tests/testcommon/ui.js
+++ b/tests/testcommon/ui.js
@@ -1,12 +1,14 @@
 // -*- mode: js; js-indent-level: 4; indent-tabs-mode: nil -*-
 
-const Config = imports.misc.config;
+import 'gi://Clutter?version=9';
+import 'gi://Gtk?version=3.0';
 
-imports.gi.versions = { Clutter: Config.LIBMUTTER_API_VERSION, Gtk: '3.0' };
+import Clutter from 'gi://Clutter';
+import Gio from 'gi://Gio';
+import GLib from 'gi://GLib';
+import St from 'gi://St';
 
-const { Clutter, Gio, GLib, St } = imports.gi;
-
-const Environment = imports.ui.environment;
+import * as Environment from '../../js/ui/environment.js';
 
 function init(stage) {
     Environment.init();
diff --git a/tests/unit/markup.js b/tests/unit/markup.js
index 603ca8194e..3527179293 100644
--- a/tests/unit/markup.js
+++ b/tests/unit/markup.js
@@ -3,12 +3,11 @@
 // Test cases for MessageList markup parsing
 
 const JsUnit = imports.jsUnit;
-const Pango = imports.gi.Pango;
+import Pango from 'gi://Pango';
 
 const Environment = imports.ui.environment;
 Environment.init();
 
-const Main = imports.ui.main; // unused, but needed to break dependency loop
 const MessageList = imports.ui.messageList;
 
 // Assert that @input, assumed to be markup, gets "fixed" to @output,


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]