diff --git a/.env b/.env
new file mode 100644
index 0000000000..0bba336491
--- /dev/null
+++ b/.env
@@ -0,0 +1 @@
+PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS=1
\ No newline at end of file
diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml
index 3cd8c3b2a0..9c7bea577b 100644
--- a/.github/workflows/playwright.yml
+++ b/.github/workflows/playwright.yml
@@ -9,8 +9,6 @@ jobs:
   playwright:
     timeout-minutes: 60
     runs-on: ubuntu-latest
-    env:
-      PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS: 1
     steps:
       - name: Remove all fonts
         run: rm -rf /usr/share/fonts
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 2480961b96..aa57bde2a3 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -6,8 +6,5 @@
         "source.fixAll.eslint": true,
     },
     "eslint.format.enable": true,
-    "playwright.env": {
-        "PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS": 1
-    },
     "javascript.preferences.importModuleSpecifierEnding": "js",
 }
diff --git a/ext/js/display/display-audio.js b/ext/js/display/display-audio.js
index 6ab5773fff..b829dd1aa7 100644
--- a/ext/js/display/display-audio.js
+++ b/ext/js/display/display-audio.js
@@ -21,11 +21,6 @@ import {PopupMenu} from '../dom/popup-menu.js';
 import {AudioSystem} from '../media/audio-system.js';
 import {yomichan} from '../yomichan.js';
 
-/* global
- * AudioSystem
- * PopupMenu
- */
-
 export class DisplayAudio {
     constructor(display) {
         this._display = display;
diff --git a/ext/js/display/popup-main.js b/ext/js/display/popup-main.js
index 0599b736b1..b82458faaa 100644
--- a/ext/js/display/popup-main.js
+++ b/ext/js/display/popup-main.js
@@ -27,17 +27,6 @@ import {DisplayProfileSelection} from './display-profile-selection.js';
 import {DisplayResizer} from './display-resizer.js';
 import {Display} from './display.js';
 
-/* global
- * Display
- * DisplayAnki
- * DisplayAudio
- * DisplayProfileSelection
- * DisplayResizer
- * DocumentFocusController
- * HotkeyHandler
- * JapaneseUtil
- */
-
 (async () => {
     try {
         const documentFocusController = new DocumentFocusController();
diff --git a/ext/js/pages/settings/dictionary-controller.js b/ext/js/pages/settings/dictionary-controller.js
index a9403e39f5..05f4f3966c 100644
--- a/ext/js/pages/settings/dictionary-controller.js
+++ b/ext/js/pages/settings/dictionary-controller.js
@@ -20,10 +20,6 @@ import {EventListenerCollection, log} from '../../core.js';
 import {DictionaryWorker} from '../../language/dictionary-worker.js';
 import {yomichan} from '../../yomichan.js';
 
-/* global
- * DictionaryWorker
- */
-
 class DictionaryEntry {
     constructor(dictionaryController, fragment, index, dictionaryInfo) {
         this._dictionaryController = dictionaryController;
diff --git a/ext/js/pages/settings/popup-preview-frame-main.js b/ext/js/pages/settings/popup-preview-frame-main.js
index d10910fc47..0b69fea045 100644
--- a/ext/js/pages/settings/popup-preview-frame-main.js
+++ b/ext/js/pages/settings/popup-preview-frame-main.js
@@ -22,12 +22,6 @@ import {HotkeyHandler} from '../../input/hotkey-handler.js';
 import {yomichan} from '../../yomichan.js';
 import {PopupPreviewFrame} from './popup-preview-frame.js';
 
-/* global
- * HotkeyHandler
- * PopupFactory
- * PopupPreviewFrame
- */
-
 (async () => {
     try {
         await yomichan.prepare();
diff --git a/ext/js/templates/sandbox/template-renderer-media-provider.js b/ext/js/templates/sandbox/template-renderer-media-provider.js
index 9d77dd1fbe..33ddec21ca 100644
--- a/ext/js/templates/sandbox/template-renderer-media-provider.js
+++ b/ext/js/templates/sandbox/template-renderer-media-provider.js
@@ -16,9 +16,7 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
-/* global
- * Handlebars
- */
+import {Handlebars} from '../../../lib/handlebars.js';
 
 export class TemplateRendererMediaProvider {
     constructor() {
diff --git a/package-lock.json b/package-lock.json
index daddedb605..03effc13b3 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -17,6 +17,7 @@
                 "css": "^3.0.0",
                 "dexie": "^3.2.4",
                 "dexie-export-import": "^4.0.7",
+                "dotenv": "^16.3.1",
                 "esbuild": "^0.19.5",
                 "eslint": "^8.52.0",
                 "eslint-plugin-header": "^3.1.1",
@@ -3412,6 +3413,18 @@
                 "node": ">=12"
             }
         },
+        "node_modules/dotenv": {
+            "version": "16.3.1",
+            "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz",
+            "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==",
+            "dev": true,
+            "engines": {
+                "node": ">=12"
+            },
+            "funding": {
+                "url": "https://github.com/motdotla/dotenv?sponsor=1"
+            }
+        },
         "node_modules/eastasianwidth": {
             "version": "0.2.0",
             "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
@@ -11158,6 +11171,12 @@
                 "webidl-conversions": "^7.0.0"
             }
         },
+        "dotenv": {
+            "version": "16.3.1",
+            "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz",
+            "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==",
+            "dev": true
+        },
         "eastasianwidth": {
             "version": "0.2.0",
             "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
diff --git a/package.json b/package.json
index 1a6ebf84f8..edb404115e 100644
--- a/package.json
+++ b/package.json
@@ -43,6 +43,7 @@
         "css": "^3.0.0",
         "dexie": "^3.2.4",
         "dexie-export-import": "^4.0.7",
+        "dotenv": "^16.3.1",
         "esbuild": "^0.19.5",
         "eslint": "^8.52.0",
         "eslint-plugin-header": "^3.1.1",
diff --git a/playwright.config.js b/playwright.config.js
index 11d79e726a..6bf645c4d1 100644
--- a/playwright.config.js
+++ b/playwright.config.js
@@ -15,18 +15,18 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 // @ts-check
-const {defineConfig, devices} = require('@playwright/test');
+import {defineConfig, devices} from '@playwright/test';
 
 /**
  * Read environment variables from file.
  * https://github.com/motdotla/dotenv
  */
-// require('dotenv').config();
+import 'dotenv/config';
 
 /**
  * @see https://playwright.dev/docs/test-configuration
  */
-module.exports = defineConfig({
+export default defineConfig({
     testDir: './test/playwright',
     snapshotPathTemplate: '{testDir}/__screenshots__/{testFilePath}/{arg}{ext}',
     /* Maximum time one test can run for. */
diff --git a/test/playwright/global.setup.js b/test/playwright/global.setup.js
index 442647f85c..1a16f1205b 100644
--- a/test/playwright/global.setup.js
+++ b/test/playwright/global.setup.js
@@ -15,11 +15,11 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
-const {test: setup} = require('@playwright/test');
-const {ManifestUtil} = require('../../dev/manifest-util');
-const {root} = require('./playwright-util');
-const path = require('path');
-const fs = require('fs');
+import {test as setup} from '@playwright/test';
+import fs from 'fs';
+import path from 'path';
+import {ManifestUtil} from '../../dev/manifest-util';
+import {root} from './playwright-util';
 
 const manifestPath = path.join(root, 'ext/manifest.json');
 const copyManifestPath = path.join(root, 'ext/manifest-old.json');
@@ -29,4 +29,4 @@ setup('use test manifest', () => {
     const variant = manifestUtil.getManifest('chrome-playwright');
     fs.renameSync(manifestPath, copyManifestPath);
     fs.writeFileSync(manifestPath, ManifestUtil.createManifestString(variant).replace('$YOMITAN_VERSION', '0.0.0.0'));
-});
\ No newline at end of file
+});
diff --git a/test/playwright/global.teardown.js b/test/playwright/global.teardown.js
index 2fb29ebe1a..6787f25550 100644
--- a/test/playwright/global.teardown.js
+++ b/test/playwright/global.teardown.js
@@ -15,14 +15,14 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
-const {test: teardown} = require('@playwright/test');
-const {root} = require('./playwright-util');
-const path = require('path');
-const fs = require('fs');
+import {test as teardown} from '@playwright/test';
+import fs from 'fs';
+import path from 'path';
+import {root} from './playwright-util';
 
 const manifestPath = path.join(root, 'ext/manifest.json');
 const copyManifestPath = path.join(root, 'ext/manifest-old.json');
 
 teardown('bring back original manifest', () => {
     fs.renameSync(copyManifestPath, manifestPath);
-});
\ No newline at end of file
+});
diff --git a/test/playwright/integration.spec.js b/test/playwright/integration.spec.js
index 1bfd39eaeb..b9a86d8460 100644
--- a/test/playwright/integration.spec.js
+++ b/test/playwright/integration.spec.js
@@ -15,18 +15,18 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
-const path = require('path');
-const {
-    test,
+import path from 'path';
+import {createDictionaryArchive} from '../../dev/util';
+import {
     expect,
-    root,
-    mockModelFieldNames,
-    mockModelFieldsToAnkiValues,
     expectedAddNoteBody,
     mockAnkiRouteHandler,
+    mockModelFieldNames,
+    mockModelFieldsToAnkiValues,
+    root,
+    test,
     writeToClipboardFromPage
-} = require('./playwright-util');
-const {createDictionaryArchive} = require('../../dev/util');
+} from './playwright-util';
 
 test.beforeEach(async ({context}) => {
     // wait for the on-install welcome.html tab to load, which becomes the foreground tab
@@ -91,4 +91,4 @@ test('anki add', async ({context, page, extensionId}) => {
     await page.locator('[data-mode="term-kanji"]').click();
     const addNoteReqBody = await addNotePromise;
     expect(addNoteReqBody).toMatchObject(expectedAddNoteBody);
-});
\ No newline at end of file
+});
diff --git a/test/playwright/playwright-util.js b/test/playwright/playwright-util.js
index e28f16eb55..5ceb92fddd 100644
--- a/test/playwright/playwright-util.js
+++ b/test/playwright/playwright-util.js
@@ -15,10 +15,12 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
-const path = require('path');
-const {test: base, chromium} = require('@playwright/test');
+import {test as base, chromium} from '@playwright/test';
+import path from 'path';
+import {fileURLToPath} from 'url';
 
-export const root = path.join(__dirname, '..', '..');
+const dirname = path.dirname(fileURLToPath(import.meta.url));
+export const root = path.join(dirname, '..', '..');
 
 export const test = base.extend({
     context: async ({ }, use) => {
@@ -106,4 +108,4 @@ const ankiRouteResponses = {
     'canAddNotes': Object.assign({body: JSON.stringify([true, true])}, baseAnkiResp),
     'storeMediaFile': Object.assign({body: JSON.stringify('mock_audio.mp3')}, baseAnkiResp),
     'addNote': Object.assign({body: JSON.stringify(102312488912)}, baseAnkiResp)
-};
\ No newline at end of file
+};
diff --git a/test/playwright/visual.spec.js b/test/playwright/visual.spec.js
index 001f329f1f..2f46990fa6 100644
--- a/test/playwright/visual.spec.js
+++ b/test/playwright/visual.spec.js
@@ -15,13 +15,13 @@
  * along with this program.  If not, see <https://www.gnu.org/licenses/>.
  */
 
-const path = require('path');
+import path from 'path';
 
-const {
-    test,
+import {
     expect,
-    root
-} = require('./playwright-util');
+    root,
+    test
+} from './playwright-util';
 
 test.beforeEach(async ({context}) => {
     // wait for the on-install welcome.html tab to load, which becomes the foreground tab
@@ -97,4 +97,4 @@ test('visual', async ({page, extensionId}) => {
         await screenshot(2, i, el, {x: 15, y: 15});
         i++;
     }
-});
\ No newline at end of file
+});