Repository: moscajs/aedes
Pull request #1039 (2025-07-15 17:35:50)

chore: migrate to ESM
This PR migrates Aedes to ESM.
I followed Sindre Sorhus' checklist mentioned by Daniel in #1038:

- [X] Add "type": "module" to your package.json 
- [X] Replace "main": "index.js" with "exports": "./index.js" in your package.json. 
- [X] Update the "engines" field in package.json to Node.js 20: "node": ">=20".
- [X] Remove 'use strict'; from all JavaScript files.
- [X] Replace all require()/module.export with import/export.
- [X] Use only full relative file paths for imports: import x from '.'; → import x from './index.js';.
- [X] If you have a TypeScript type definition (for example, index.d.ts), update it to use ESM imports/exports.
- [X] Use the [node: protocol](https://nodejs.org/api/esm.html#esm_node_imports) for Node.js built-in imports.

And added the following items:
- [X] remove `proxyquire` as it does not work with ESM (Proxy-require, the name says it all)
- [X] monkey patch neostandard in eslint.config.js as it does not support import assertions yet (I opened a ticket with them, but this works)
- [X] add `test/require.cjs` which checks if `require("aedes")` still works for those that need it.

Enjoy!
Hans
For reference: I used `ts2esm` with a simple `jsconfig.json` to do most of the heavy lifting.


Patch:
diff --git a/aedes.js b/aedes.js
index 9c1668a..57b53a9 100644
--- a/aedes.js
+++ b/aedes.js
@@ -1,16 +1,15 @@
-'use strict'
-
-const EventEmitter = require('events')
-const parallel = require('fastparallel')
-const series = require('fastseries')
-const { v4: uuidv4 } = require('uuid')
-const reusify = require('reusify')
-const { pipeline } = require('stream')
-const Packet = require('aedes-packet')
-const memory = require('aedes-persistence')
-const mqemitter = require('mqemitter')
-const Client = require('./lib/client')
-const { $SYS_PREFIX, bulk } = require('./lib/utils')
+import EventEmitter from 'node:events'
+import parallel from 'fastparallel'
+import series from 'fastseries'
+import { v4 as uuidv4 } from 'uuid'
+import reusify from 'reusify'
+import { pipeline } from 'stream'
+import Packet from 'aedes-packet'
+import memory from 'aedes-persistence'
+import mqemitter from 'mqemitter'
+import Client from './lib/client.js'
+import { $SYS_PREFIX, bulk } from './lib/utils.js'
+import pkg from './package.json' with { type: 'json' }
 
 const defaultOptions = {
   concurrency: 100,
@@ -29,10 +28,9 @@ const defaultOptions = {
   maxClientsIdLength: 23,
   keepaliveLimit: 0
 }
+const version = pkg.version
 
-const version = require('./package.json').version
-
-class Aedes extends EventEmitter {
+export class Aedes extends EventEmitter {
   constructor (opts) {
     super()
     const that = this
@@ -412,6 +410,4 @@ function warnMigrate () {
  `)
 }
 
-module.exports = warnMigrate
-module.exports.createBroker = Aedes.createBroker
-module.exports.Aedes = Aedes
+export default warnMigrate
diff --git a/benchmarks/pingpong.js b/benchmarks/pingpong.js
index 12fb8bc..b336b7a 100755
--- a/benchmarks/pingpong.js
+++ b/benchmarks/pingpong.js
@@ -1,8 +1,7 @@
 #! /usr/bin/env node
-const { parseArgs } = require('node:util')
-const { hrtime } = require('node:process')
-const mqtt = require('mqtt')
-
+import { parseArgs } from 'node:util'
+import { hrtime } from 'node:process'
+import mqtt from 'mqtt'
 const interval = 5000
 
 function processsLatencies (latencies, counter) {
diff --git a/benchmarks/receiver.js b/benchmarks/receiver.js
index 0f16ce4..6d4d8bf 100755
--- a/benchmarks/receiver.js
+++ b/benchmarks/receiver.js
@@ -1,7 +1,6 @@
 #! /usr/bin/env node
-const { parseArgs } = require('node:util')
-const mqtt = require('mqtt')
-
+import { parseArgs } from 'node:util'
+import mqtt from 'mqtt'
 const interval = 5000
 
 let counter = 0
diff --git a/benchmarks/report.js b/benchmarks/report.js
index d95871b..518ce27 100644
--- a/benchmarks/report.js
+++ b/benchmarks/report.js
@@ -1,7 +1,6 @@
 #! /usr/bin/env node
 
-const readline = require('readline')
-
+import readline from 'readline'
 // if any average is more than threshold% off we exit with 1
 const threshold = 10
 let failed = false
diff --git a/benchmarks/runBenchmarks.js b/benchmarks/runBenchmarks.js
index 9719ed6..24a7698 100644
--- a/benchmarks/runBenchmarks.js
+++ b/benchmarks/runBenchmarks.js
@@ -1,16 +1,16 @@
-const { fork, execSync } = require('node:child_process')
-const { cpus } = require('node:os')
-const path = require('node:path')
-
+import { fork, execSync } from 'node:child_process'
+import { cpus } from 'node:os'
+import path from 'node:path'
 const gitBranch = execSync('git rev-parse --abbrev-ref HEAD').toString().trim()
 const numCores = cpus().length
 const cpuType = cpus()[0].model.trim()
+const dirname = import.meta.dirname
 
 const scripts = {
-  server: path.join(__dirname, 'server.js'),
-  sender: path.join(__dirname, 'sender.js'),
-  receiver: path.join(__dirname, 'receiver.js'),
-  pingpong: path.join(__dirname, 'pingpong.js'),
+  server: path.join(dirname, 'server.js'),
+  sender: path.join(dirname, 'sender.js'),
+  receiver: path.join(dirname, 'receiver.js'),
+  pingpong: path.join(dirname, 'pingpong.js'),
 }
 
 function spawn (script, cmdArgs = [], msgType = 'rate', args = {}) {
diff --git a/benchmarks/sender.js b/benchmarks/sender.js
index a70cd48..5600661 100755
--- a/benchmarks/sender.js
+++ b/benchmarks/sender.js
@@ -1,8 +1,7 @@
 #! /usr/bin/env node
 
-const mqtt = require('mqtt')
-const { parseArgs } = require('node:util')
-
+import mqtt from 'mqtt'
+import { parseArgs } from 'node:util'
 const interval = 5000
 
 let sent = 0
diff --git a/benchmarks/server.js b/benchmarks/server.js
index 2535df0..f9597b2 100644
--- a/benchmarks/server.js
+++ b/benchmarks/server.js
@@ -1,11 +1,9 @@
-'use strict'
+import { Aedes } from '../aedes.js'
+import { createServer } from 'net'
 
 // To be used with cpuprofilify http://npm.im/cpuprofilify
-
-const { Aedes } = require('../')
-
 Aedes.createBroker().then(aedes => {
-  const server = require('net').createServer(aedes.handle)
+  const server = createServer(aedes.handle)
   const port = 1883
 
   server.listen(port, function () {
diff --git a/docs/Aedes.md b/docs/Aedes.md
index f71f441..b8cc67c 100644
--- a/docs/Aedes.md
+++ b/docs/Aedes.md
@@ -203,10 +203,11 @@ Emitted when server is closed.
 A connection listener that pipe stream to aedes.
 
 ```js
-const { Aedes } = require('/aedes')
-const aedes = await Aedes.createBroker()
+import { createServer } from 'node:net'
+import { Aedes } from 'aedes'
 
-const server = require('net').createServer(aedes.handle)
+const aedes = await Aedes.createBroker()
+const server = createServer(aedes.handle)
 ```
 
 ## aedes.subscribe (topic, deliverfunc, callback)
diff --git a/docs/Examples.md b/docs/Examples.md
index ac70ab7..4a82b2e 100644
--- a/docs/Examples.md
+++ b/docs/Examples.md
@@ -4,12 +4,12 @@
 ## Simple plain MQTT server
 
 ```js
+import { createServer } from 'node:net'
+import Aedes from 'aedes'
 
-const { Aedes } = require('aedes')
-const { createServer } = require('net')
+const port = 1883
 
 const aedes = await Aedes.createBroker()
-const port = 1883
 const server = createServer(aedes.handle)
 
 server.listen(port, function () {
@@ -17,15 +17,14 @@ server.listen(port, function () {
 })
 ```
 
-## Typescript
+## CommonJS
 
-```ts
-import Aedes from 'aedes'
-import { createServer } from 'net'
-
-const port = 1883
+```js
+const { createServer } = require('node:net')
+const { Aedes } = require('aedes')
 
 const aedes = await Aedes.createBroker()
+const port = 1883
 const server = createServer(aedes.handle)
 
 server.listen(port, function () {
@@ -36,8 +35,8 @@ server.listen(port, function () {
 ## Simple plain MQTT server using server-factory
 
 ```js
-const { Aedes } = require('aedes')
-const { createServer } = require('aedes-server-factory')
+import { Aedes } from 'aedes'
+import { createServer } from 'aedes-server-factory'
 const port = 1883
 
 const aedes = await Aedes.createBroker()
@@ -51,16 +50,18 @@ server.listen(port, function () {
 ## MQTT over TLS / MQTTS
 
 ```js
-const fs = require('fs')
-const { Aedes } = require('aedes')
+import { createServer } from 'node:tls'
+import { readFileSync } from 'node:fs'
+import { Aedes } from 'aedes'
+
 const port = 8883
 
 const options = {
-  key: fs.readFileSync('YOUR_PRIVATE_KEY_FILE.pem'),
-  cert: fs.readFileSync('YOUR_PUBLIC_CERT_FILE.pem')
+  key: readFileSync('YOUR_PRIVATE_KEY_FILE.pem'),
+  cert: readFileSync('YOUR_PUBLIC_CERT_FILE.pem')
 }
 const aedes = await Aedes.createBroker()
-const server = require('tls').createServer(options, aedes.handle)
+const server = createServer(options, aedes.handle)
 
 server.listen(port, function () {
   console.log('server started and listening on port ', port)
@@ -70,19 +71,20 @@ server.listen(port, function () {
 ## MQTT server over WebSocket
 
 ```js
-const { Aedes } = require('aedes')
-const { createServer } require('http')
-const ws = require('ws')
+import { createServer } from 'node:http'
+import { Aedes } from 'aedes'
+import { WebSocketServer, createWebSocketStream } from 'ws'
+
 const port = 8888
 
 const aedes = await Aedes.createBroker()
 const httpServer = createServer()
-const wss = new ws.WebSocketServer({
+const wss = new WebSocketServer({
   server:httpServer
 })
 
 wss.on('connection', (websocket, req) => {
-  const stream = ws.createWebSocketStream(websocket)
+  const stream = createWebSocketStream(websocket)
   aedes.handle(stream, req)
 })
 
@@ -94,8 +96,8 @@ httpServer.listen(port, function () {
 ## MQTT server over WebSocket using server-factory
 
 ```js
-const { Aedes } = require('aedes')
-const { createServer } = require('aedes-server-factory')
+import { Aedes } from 'aedes'
+import { createServer } from 'aedes-server-factory'
 const port = 8888
 
 const aedes = await Aedes.createBroker()
diff --git a/eslint.config.js b/eslint.config.js
index 6a56807..93a3aaf 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -1,3 +1,8 @@
-'use strict'
-
-module.exports = require('neostandard')({ ts: true })
+import neostandard from 'neostandard'
+const ns = neostandard({ ts: true })
+for (const item of ns) {
+  if (item?.languageOptions?.ecmaVersion < 2025) {
+    item.languageOptions.ecmaVersion = 2025
+  }
+}
+export default ns
diff --git a/example.js b/example.js
index eac602e..d5b6273 100644
--- a/example.js
+++ b/example.js
@@ -1,9 +1,7 @@
-'use strict'
-
-const net = require('node:net')
-const http = require('node:http')
-const { Aedes } = require('./aedes')
-const ws = require('ws')
+import net from 'node:net'
+import http from 'node:http'
+import { Aedes } from './aedes.js'
+import ws from 'ws'
 
 const port = 1883
 const wsPort = 8888
diff --git a/examples/clusters/index.js b/examples/clusters/index.js
index b633f4a..9aa837d 100644
--- a/examples/clusters/index.js
+++ b/examples/clusters/index.js
@@ -1,28 +1,33 @@
-const cluster = require('cluster')
-const { Aedes } = require('aedes')
-const { createServer } = require('net')
-const { cpus } = require('os')
+import cluster from 'node:cluster'
+import { createServer } from 'node:net'
+import { cpus } from 'node:os'
+import { Aedes } from '../../aedes.js'
 const MONGO_URL = 'mongodb://127.0.0.1/aedes-clusters'
 
-const mq = process.env.MQ === 'redis'
-  ? require('mqemitter-redis')({
-    port: process.env.REDIS_PORT || 6379
-  })
-  : require('mqemitter-mongodb')({
-    url: MONGO_URL
-  })
-
-const persistence = process.env.PERSISTENCE === 'redis'
-  ? require('aedes-persistence-redis')({
-    port: process.env.REDIS_PORT || 6379
-  })
-  : require('aedes-persistence-mongodb')({
-    url: MONGO_URL
-  })
+async function importAndCall (mod, params) {
+  const imported = await import(mod)
+  return imported.default(params)
+}
 
 async function startAedes () {
   const port = 1883
 
+  const mq = process.env.MQ === 'redis'
+    ? await importAndCall('mqemitter-redis', {
+      port: process.env.REDIS_PORT || 6379
+    })
+    : await importAndCall('mqemitter-mongodb', {
+      url: MONGO_URL
+    })
+
+  const persistence = process.env.PERSISTENCE === 'redis'
+    ? await importAndCall('aedes-persistence-redis', {
+      port: process.env.REDIS_PORT || 6379
+    })
+    : await importAndCall('aedes-persistence-mongodb', {
+      url: MONGO_URL
+    })
+
   const aedes = await Aedes.createBroker({
     id: 'BROKER_' + cluster.worker.id,
     mq,
@@ -43,12 +48,12 @@ async function startAedes () {
 
   aedes.on('subscribe', function (subscriptions, client) {
     console.log('MQTT client \x1b[32m' + (client ? client.id : client) +
-            '\x1b[0m subscribed to topics: ' + subscriptions.map(s => s.topic).join('\n'), 'from broker', aedes.id)
+      '\x1b[0m subscribed to topics: ' + subscriptions.map(s => s.topic).join('\n'), 'from broker', aedes.id)
   })
 
   aedes.on('unsubscribe', function (subscriptions, client) {
     console.log('MQTT client \x1b[32m' + (client ? client.id : client) +
-            '\x1b[0m unsubscribed to topics: ' + subscriptions.join('\n'), 'from broker', aedes.id)
+      '\x1b[0m unsubscribed to topics: ' + subscriptions.join('\n'), 'from broker', aedes.id)
   })
 
   // fired when a client connects
diff --git a/examples/clusters/package.json b/examples/clusters/package.json
index 6a5dd09..e898a6d 100644
--- a/examples/clusters/package.json
+++ b/examples/clusters/package.json
@@ -2,17 +2,21 @@
   "name": "aedes_clusters",
   "version": "1.0.0",
   "description": "Testing Aedes Broker with clusters",
-  "main": "index.js",
+  "type": "module",
+  "exports": "./index.js",
+  "engines": {
+		"node": ">=20"
+	},
   "scripts": {
     "test": "echo \"Error: no test specified\" && exit 1"
   },
   "author": "robertsLando",
   "license": "MIT",
   "dependencies": {
-    "aedes": "^0.48.1",
-    "aedes-persistence-mongodb": "^9.1.0",
-    "aedes-persistence-redis": "^9.0.1",
-    "mqemitter-mongodb": "^8.1.0",
-    "mqemitter-redis": "^5.0.0"
+    "aedes": "^0.51.3",
+    "aedes-persistence-mongodb": "^9.3.1",
+    "aedes-persistence-redis": "^11.2.1",
+    "mqemitter-mongodb": "^9.0.1",
+    "mqemitter-redis": "^7.1.0"
   }
 }
diff --git a/lib/client.js b/lib/client.js
index df917d5..213808e 100644
--- a/lib/client.js
+++ b/lib/client.js
@@ -1,17 +1,15 @@
-'use strict'
-
-const mqtt = require('mqtt-packet')
-const EventEmitter = require('events')
-const util = require('util')
-const eos = require('end-of-stream')
-const Packet = require('aedes-packet')
-const write = require('./write')
-const QoSPacket = require('./qos-packet')
-const handleSubscribe = require('./handlers/subscribe')
-const handleUnsubscribe = require('./handlers/unsubscribe')
-const handle = require('./handlers')
-const { pipeline } = require('stream')
-const { through } = require('./utils')
+import mqtt from 'mqtt-packet'
+import EventEmitter from 'node:events'
+import util from 'util'
+import eos from 'end-of-stream'
+import Packet from 'aedes-packet'
+import write from './write.js'
+import QoSPacket from './qos-packet.js'
+import handleSubscribe from './handlers/subscribe.js'
+import handleUnsubscribe from './handlers/unsubscribe.js'
+import handle from './handlers/index.js'
+import { pipeline } from 'stream'
+import { through } from './utils.js'
 
 class Client {
   constructor (broker, conn, req) {
@@ -394,4 +392,5 @@ function dequeue () {
 }
 
 function noop () {}
-module.exports = Client
+
+export default Client
diff --git a/lib/handlers/connect.js b/lib/handlers/connect.js
index 0d42196..b7e2129 100644
--- a/lib/handlers/connect.js
+++ b/lib/handlers/connect.js
@@ -1,12 +1,12 @@
-'use strict'
+import retimer from 'retimer'
+import { pipeline } from 'stream'
+import write from '../write.js'
+import QoSPacket from '../qos-packet.js'
+import { through } from '../utils.js'
+import handleSubscribe from './subscribe.js'
+import hyperid from 'hyperid'
 
-const retimer = require('retimer')
-const { pipeline } = require('stream')
-const write = require('../write')
-const QoSPacket = require('../qos-packet')
-const { through } = require('../utils')
-const handleSubscribe = require('./subscribe')
-const uniqueId = require('hyperid')()
+const uniqueId = hyperid()
 
 class Connack {
   constructor (arg) {
@@ -273,4 +273,4 @@ function emptyQueueFilter (err, client, packet) {
   }
 }
 
-module.exports = handleConnect
+export default handleConnect
diff --git a/lib/handlers/index.js b/lib/handlers/index.js
index a5dfaa8..f3d4f57 100644
--- a/lib/handlers/index.js
+++ b/lib/handlers/index.js
@@ -1,13 +1,11 @@
-'use strict'
-
-const handleConnect = require('./connect')
-const handleSubscribe = require('./subscribe')
-const handleUnsubscribe = require('./unsubscribe')
-const handlePublish = require('./publish')
-const handlePuback = require('./puback')
-const handlePubrel = require('./pubrel')
-const handlePubrec = require('./pubrec')
-const handlePing = require('./ping')
+import handleConnect from './connect.js'
+import handleSubscribe from './subscribe.js'
+import handleUnsubscribe from './unsubscribe.js'
+import handlePublish from './publish.js'
+import handlePuback from './puback.js'
+import handlePubrel from './pubrel.js'
+import handlePubrec from './pubrec.js'
+import handlePing from './ping.js'
 
 function handle (client, packet, done) {
   if (packet.cmd === 'connect') {
@@ -74,4 +72,4 @@ function finish (conn, packet, done) {
   done(error)
 }
 
-module.exports = handle
+export default handle
diff --git a/lib/handlers/ping.js b/lib/handlers/ping.js
index a4c042c..caa2814 100644
--- a/lib/handlers/ping.js
+++ b/lib/handlers/ping.js
@@ -1,6 +1,5 @@
-'use strict'
+import write from '../write.js'
 
-const write = require('../write')
 const pingResp = {
   cmd: 'pingresp'
 }
@@ -10,4 +9,4 @@ function handlePing (client, packet, done) {
   write(client, pingResp, done)
 }
 
-module.exports = handlePing
+export default handlePing
diff --git a/lib/handlers/puback.js b/lib/handlers/puback.js
index cb494c7..c73960f 100644
--- a/lib/handlers/puback.js
+++ b/lib/handlers/puback.js
@@ -1,5 +1,3 @@
-'use strict'
-
 function handlePuback (client, packet, done) {
   const persistence = client.broker.persistence
   const emitAck = (err, origPacket) => {
@@ -10,4 +8,4 @@ function handlePuback (client, packet, done) {
     .then((packet) => emitAck(null, packet), emitAck)
 }
 
-module.exports = handlePuback
+export default handlePuback
diff --git a/lib/handlers/publish.js b/lib/handlers/publish.js
index eb7f72d..69b201c 100644
--- a/lib/handlers/publish.js
+++ b/lib/handlers/publish.js
@@ -1,6 +1,4 @@
-'use strict'
-
-const write = require('../write')
+import write from '../write.js'
 
 class PubAck {
   constructor (packet) {
@@ -64,4 +62,4 @@ function authorizePublish (packet, done) {
   this.broker.authorizePublish(this, packet, done)
 }
 
-module.exports = handlePublish
+export default handlePublish
diff --git a/lib/handlers/pubrec.js b/lib/handlers/pubrec.js
index fadde43..c1b0024 100644
--- a/lib/handlers/pubrec.js
+++ b/lib/handlers/pubrec.js
@@ -1,6 +1,4 @@
-'use strict'
-
-const write = require('../write')
+import write from '../write.js'
 
 class PubRel {
   constructor (packet) {
@@ -21,4 +19,4 @@ function handlePubrec (client, packet, done) {
     .then(() => write(client, pubrel, done), done)
 }
 
-module.exports = handlePubrec
+export default handlePubrec
diff --git a/lib/handlers/pubrel.js b/lib/handlers/pubrel.js
index 9ef1612..4af4e27 100644
--- a/lib/handlers/pubrel.js
+++ b/lib/handlers/pubrel.js
@@ -1,6 +1,4 @@
-'use strict'
-
-const write = require('../write')
+import write from '../write.js'
 
 class ClientPacketStatus {
   constructor (client, packet) {
@@ -53,4 +51,4 @@ function pubrelDel (arg, done) {
     .then(() => done(null), done)
 }
 
-module.exports = handlePubrel
+export default handlePubrel
diff --git a/lib/handlers/subscribe.js b/lib/handlers/subscribe.js
index d1e9ba4..d5afa5e 100644
--- a/lib/handlers/subscribe.js
+++ b/lib/handlers/subscribe.js
@@ -1,10 +1,7 @@
-'use strict'
-
-const fastfall = require('fastfall')
-const Packet = require('aedes-packet')
-const { through } = require('../utils')
-const { validateTopic, $SYS_PREFIX } = require('../utils')
-const write = require('../write')
+import fastfall from 'fastfall'
+import Packet from 'aedes-packet'
+import { through, validateTopic, $SYS_PREFIX } from '../utils.js'
+import write from '../write.js'
 
 const subscribeTopicActions = fastfall([
   authorize,
@@ -259,4 +256,4 @@ function completeSubscribe (err) {
 
 function noop () { }
 
-module.exports = handleSubscribe
+export default handleSubscribe
diff --git a/lib/handlers/unsubscribe.js b/lib/handlers/unsubscribe.js
index e83f22a..9fd3d72 100644
--- a/lib/handlers/unsubscribe.js
+++ b/lib/handlers/unsubscribe.js
@@ -1,7 +1,5 @@
-'use strict'
-
-const write = require('../write')
-const { validateTopic, $SYS_PREFIX } = require('../utils')
+import write from '../write.js'
+import { validateTopic, $SYS_PREFIX } from '../utils.js'
 
 class UnSubAck {
   constructor (packet) {
@@ -100,4 +98,4 @@ function completeUnsubscribe (err) {
 
 function noop () { }
 
-module.exports = handleUnsubscribe
+export default handleUnsubscribe
diff --git a/lib/qos-packet.js b/lib/qos-packet.js
index ebace14..cebf579 100644
--- a/lib/qos-packet.js
+++ b/lib/qos-packet.js
@@ -1,6 +1,4 @@
-'use strict'
-
-const Packet = require('aedes-packet')
+import Packet from 'aedes-packet'
 
 class QoSPacket extends Packet {
   constructor (original, client) {
@@ -21,4 +19,4 @@ class QoSPacket extends Packet {
   }
 }
 
-module.exports = QoSPacket
+export default QoSPacket
diff --git a/lib/utils.js b/lib/utils.js
index 4ebd34f..78eca80 100644
--- a/lib/utils.js
+++ b/lib/utils.js
@@ -1,8 +1,6 @@
-'use strict'
+import { Transform, Writable } from 'stream'
 
-const { Transform, Writable } = require('stream')
-
-function validateTopic (topic, message) {
+export function validateTopic (topic, message) {
   if (!topic || topic.length === 0) { // [MQTT-3.8.3-3]
     return new Error('impossible to ' + message + ' to an empty topic')
   }
@@ -32,14 +30,14 @@ function validateTopic (topic, message) {
   }
 }
 
-function through (transform) {
+export function through (transform) {
   return new Transform({
     objectMode: true,
     transform
   })
 }
 
-function bulk (fn) {
+export function bulk (fn) {
   return new Writable({
     objectMode: true,
     writev: function (chunks, cb) {
@@ -48,9 +46,4 @@ function bulk (fn) {
   })
 }
 
-module.exports = {
-  validateTopic,
-  through,
-  bulk,
-  $SYS_PREFIX: '$SYS/'
-}
+export const $SYS_PREFIX = '$SYS/'
diff --git a/lib/write.js b/lib/write.js
index 716d81a..8cc7db9 100644
--- a/lib/write.js
+++ b/lib/write.js
@@ -1,6 +1,4 @@
-'use strict'
-
-const mqtt = require('mqtt-packet')
+import mqtt from 'mqtt-packet'
 
 function write (client, packet, done) {
   let error = null
@@ -21,4 +19,4 @@ function write (client, packet, done) {
   setImmediate(done, error, client)
 }
 
-module.exports = write
+export default write
diff --git a/package.json b/package.json
index ffb6d4e..ad2d3d6 100644
--- a/package.json
+++ b/package.json
@@ -1,153 +1,152 @@
 {
-  "name": "aedes",
-  "version": "0.51.3",
-  "description": "Stream-based MQTT broker",
-  "main": "aedes.js",
-  "type": "commonjs",
-  "types": "aedes.d.ts",
-  "scripts": {
-    "lint": "npm run lint:code && npm run lint:markdown",
-    "lint:fix": "eslint --fix",
-    "lint:code": "eslint",
-    "lint:markdown": "markdownlint docs/*.md README.md",
-    "lint:markdown:fix": "markdownlint --fix docs/*.md README.md",
-    "test": "npm run lint && npm run unit && npm run test:typescript",
-    "test:ci": "npm run lint && npm run unit -- --cov --no-check-coverage --coverage-report=lcovonly && npm run test:typescript",
-    "test:report": "npm run lint && npm run unit:report && npm run test:typescript",
-    "test:typescript": "tsd",
-    "unit": "tap -J test/*.js",
-    "unit:report": "tap -J test/*.js --cov --coverage-report=html --coverage-report=cobertura | tee out.tap",
-    "license-checker": "license-checker --production --onlyAllow=\"MIT;ISC;BSD-3-Clause;BSD-2-Clause;0BSD\"",
-    "release": "read -p 'GITHUB_TOKEN: ' GITHUB_TOKEN && export GITHUB_TOKEN=$GITHUB_TOKEN && release-it --disable-metrics"
-  },
-  "release-it": {
-    "github": {
-      "release": true
-    },
-    "git": {
-      "tagName": "v${version}"
-    },
-    "hooks": {
-      "before:init": [
-        "npm run test:ci"
-      ]
-    },
-    "npm": {
-      "publish": true
-    }
-  },
-  "pre-commit": [
-    "test"
-  ],
-  "tsd": {
-    "directory": "test/types"
-  },
-  "standard": {
-    "ignore": [
-      "types/*",
-      "test/types/*"
-    ]
-  },
-  "repository": {
-    "type": "git",
-    "url": "git+https://github.com/moscajs/aedes.git"
-  },
-  "keywords": [
-    "mqtt",
-    "broker",
-    "server",
-    "mqtt-server",
-    "stream",
-    "streams",
-    "publish",
-    "subscribe",
-    "pubsub",
-    "messaging",
-    "mosca",
-    "mosquitto",
-    "iot",
-    "internet",
-    "of",
-    "things"
-  ],
-  "author": "Matteo Collina <hello@matteocollina.com>",
-  "contributors": [
-    {
-      "name": "Gavin D'mello",
-      "url": "https://github.com/GavinDmello"
-    },
-    {
-      "name": "Behrad Zari",
-      "url": "https://github.com/behrad"
-    },
-    {
-      "name": "Gnought",
-      "url": "https://github.com/gnought"
-    },
-    {
-      "name": "Daniel Lando",
-      "url": "https://github.com/robertsLando"
-    }
-  ],
-  "license": "MIT",
-  "funding": {
-    "type": "opencollective",
-    "url": "https://opencollective.com/aedes"
-  },
-  "bugs": {
-    "url": "https://github.com/moscajs/aedes/issues"
-  },
-  "homepage": "https://github.com/moscajs/aedes#readme",
-  "engines": {
-    "node": ">=20"
-  },
-  "devDependencies": {
-    "@fastify/pre-commit": "^2.2.0",
-    "@sinonjs/fake-timers": "^11.2.2",
-    "@types/node": "^24.0.12",
-    "concat-stream": "^2.0.0",
-    "duplexify": "^4.1.3",
-    "eslint": "^9.30.1",
-    "license-checker": "^25.0.1",
-    "markdownlint-cli": "^0.45.0",
-    "mqtt": "^5.13.2",
-    "mqtt-connection": "^4.1.0",
-    "neostandard": "^0.12.2",
-    "proxyquire": "^2.1.3",
-    "release-it": "^19.0.3",
-    "snazzy": "^9.0.0",
-    "tap": "^16.3.10",
-    "tsd": "^0.32.0",
-    "typescript": "^5.8.3",
-    "ws": "^8.18.3"
-  },
-  "dependencies": {
-    "aedes-packet": "^3.0.0",
-    "aedes-persistence": "^10.2.2",
-    "end-of-stream": "^1.4.5",
-    "fastfall": "^1.5.1",
-    "fastparallel": "^2.4.1",
-    "fastseries": "^2.0.0",
-    "hyperid": "^3.3.0",
-    "mqemitter": "^7.0.0",
-    "mqtt-packet": "^9.0.2",
-    "retimer": "^4.0.0",
-    "reusify": "^1.1.0",
-    "uuid": "^11.1.0"
-  },
-  "peerDependencies": {
-    "aedes-persistence-mongodb": "^9.3.1",
-    "aedes-persistence-redis": "^11.2.1"
-  },
-  "peerDependenciesMeta": {
-    "aedes-persistence-level": {
-      "optional": true
-    },
-    "aedes-persistence-mongodb": {
-      "optional": true
-    },
-    "aedes-persistence-redis": {
-      "optional": true
-    }
-  }
+	"name": "aedes",
+	"version": "0.51.3",
+	"description": "Stream-based MQTT broker",
+	"exports": "./aedes.js",
+	"type": "module",
+	"types": "aedes.d.ts",
+	"scripts": {
+		"lint": "npm run lint:code && npm run lint:markdown",
+		"lint:fix": "eslint --fix",
+		"lint:code": "eslint",
+		"lint:markdown": "markdownlint docs/*.md README.md",
+		"lint:markdown:fix": "markdownlint --fix docs/*.md README.md",
+		"test": "npm run lint && npm run unit && npm run test:typescript",
+		"test:ci": "npm run lint && npm run unit -- --cov --no-check-coverage --coverage-report=lcovonly && npm run test:typescript",
+		"test:report": "npm run lint && npm run unit:report && npm run test:typescript",
+		"test:typescript": "tsd",
+		"unit": "tap -J test/*.js",
+		"unit:report": "tap -J test/*.js --cov --coverage-report=html --coverage-report=cobertura | tee out.tap",
+		"license-checker": "license-checker --production --onlyAllow=\"MIT;ISC;BSD-3-Clause;BSD-2-Clause;0BSD\"",
+		"release": "read -p 'GITHUB_TOKEN: ' GITHUB_TOKEN && export GITHUB_TOKEN=$GITHUB_TOKEN && release-it --disable-metrics"
+	},
+	"release-it": {
+		"github": {
+			"release": true
+		},
+		"git": {
+			"tagName": "v${version}"
+		},
+		"hooks": {
+			"before:init": [
+				"npm run test:ci"
+			]
+		},
+		"npm": {
+			"publish": true
+		}
+	},
+	"pre-commit": [
+		"test"
+	],
+	"tsd": {
+		"directory": "test/types"
+	},
+	"standard": {
+		"ignore": [
+			"types/*",
+			"test/types/*"
+		]
+	},
+	"repository": {
+		"type": "git",
+		"url": "git+https://github.com/moscajs/aedes.git"
+	},
+	"keywords": [
+		"mqtt",
+		"broker",
+		"server",
+		"mqtt-server",
+		"stream",
+		"streams",
+		"publish",
+		"subscribe",
+		"pubsub",
+		"messaging",
+		"mosca",
+		"mosquitto",
+		"iot",
+		"internet",
+		"of",
+		"things"
+	],
+	"author": "Matteo Collina <hello@matteocollina.com>",
+	"contributors": [
+		{
+			"name": "Gavin D'mello",
+			"url": "https://github.com/GavinDmello"
+		},
+		{
+			"name": "Behrad Zari",
+			"url": "https://github.com/behrad"
+		},
+		{
+			"name": "Gnought",
+			"url": "https://github.com/gnought"
+		},
+		{
+			"name": "Daniel Lando",
+			"url": "https://github.com/robertsLando"
+		}
+	],
+	"license": "MIT",
+	"funding": {
+		"type": "opencollective",
+		"url": "https://opencollective.com/aedes"
+	},
+	"bugs": {
+		"url": "https://github.com/moscajs/aedes/issues"
+	},
+	"homepage": "https://github.com/moscajs/aedes#readme",
+	"engines": {
+		"node": ">=20"
+	},
+	"devDependencies": {
+		"@fastify/pre-commit": "^2.2.0",
+		"@sinonjs/fake-timers": "^11.2.2",
+		"@types/node": "^24.0.12",
+		"concat-stream": "^2.0.0",
+		"duplexify": "^4.1.3",
+		"eslint": "^9.30.1",
+		"license-checker": "^25.0.1",
+		"markdownlint-cli": "^0.45.0",
+		"mqtt": "^5.13.2",
+		"mqtt-connection": "^4.1.0",
+		"neostandard": "^0.12.2",
+		"release-it": "^19.0.3",
+		"snazzy": "^9.0.0",
+		"tap": "^16.3.10",
+		"tsd": "^0.32.0",
+		"typescript": "^5.8.3",
+		"ws": "^8.18.3"
+	},
+	"dependencies": {
+		"aedes-packet": "^3.0.0",
+		"aedes-persistence": "^10.2.2",
+		"end-of-stream": "^1.4.5",
+		"fastfall": "^1.5.1",
+		"fastparallel": "^2.4.1",
+		"fastseries": "^2.0.0",
+		"hyperid": "^3.3.0",
+		"mqemitter": "^7.0.0",
+		"mqtt-packet": "^9.0.2",
+		"retimer": "^4.0.0",
+		"reusify": "^1.1.0",
+		"uuid": "^11.1.0"
+	},
+	"peerDependencies": {
+		"aedes-persistence-mongodb": "^9.3.1",
+		"aedes-persistence-redis": "^11.2.1"
+	},
+	"peerDependenciesMeta": {
+		"aedes-persistence-level": {
+			"optional": true
+		},
+		"aedes-persistence-mongodb": {
+			"optional": true
+		},
+		"aedes-persistence-redis": {
+			"optional": true
+		}
+	}
 }
diff --git a/types/instance.d.ts b/types/instance.d.ts
index 13dc4b5..bcce4a7 100644
--- a/types/instance.d.ts
+++ b/types/instance.d.ts
@@ -1,6 +1,6 @@
 import { Duplex } from 'node:stream'
 import { Socket } from 'node:net'
-import { IncomingMessage } from 'http'
+import { IncomingMessage } from 'node:http'
 import { Client } from './client'
 import type {
   AedesPublishPacket,


Test patch:
diff --git a/test/auth.js b/test/auth.js
index 7aafef8..45de50c 100644
--- a/test/auth.js
+++ b/test/auth.js
@@ -1,11 +1,9 @@
-'use strict'
-
-const { test } = require('tap')
-const eos = require('end-of-stream')
-const Faketimers = require('@sinonjs/fake-timers')
-const Client = require('../lib/client')
-const { setup, connect, noError, subscribe, subscribeMultiple } = require('./helper')
-const { Aedes } = require('../')
+import { test } from 'tap'
+import eos from 'end-of-stream'
+import Faketimers from '@sinonjs/fake-timers'
+import Client from '../lib/client.js'
+import { setup, connect, noError, subscribe, subscribeMultiple } from './helper.js'
+import { Aedes } from '../aedes.js'
 
 test('authenticate successfully a client with username and password', function (t) {
   t.plan(4)
diff --git a/test/basic.js b/test/basic.js
index 2dbabc1..7eba0b9 100644
--- a/test/basic.js
+++ b/test/basic.js
@@ -1,11 +1,8 @@
-'use strict'
-
-const { test } = require('tap')
-const eos = require('end-of-stream')
-const { setup, connect, subscribe, subscribeMultiple, noError } = require('./helper')
-const { Aedes } = require('../')
-const defaultExport = require('../')
-const proxyquire = require('proxyquire')
+import { test } from 'tap'
+import eos from 'end-of-stream'
+import { setup, connect, subscribe, subscribeMultiple, noError } from './helper.js'
+import defaultExport, { Aedes } from '../aedes.js'
+import write from '../lib/write.js'
 
 test('test Aedes constructor', function (t) {
   t.plan(1)
@@ -193,17 +190,10 @@ test('publish to $SYS topic throws error', function (t) {
 test('return write errors to callback', function (t) {
   t.plan(1)
 
-  const write = proxyquire('../lib/write.js', {
-    'mqtt-packet': {
-      writeToStream: () => {
-        throw Error('error')
-      }
-    }
-  })
-
   const client = {
     conn: {
-      writable: true
+      writable: true,
+      write: () => { throw new Error('error') }
     },
     connecting: true
   }
diff --git a/test/bridge.js b/test/bridge.js
index 87ca9a4..abe6a6a 100644
--- a/test/bridge.js
+++ b/test/bridge.js
@@ -1,8 +1,6 @@
-'use strict'
-
-const { test } = require('tap')
-const { setup, connect, subscribe } = require('./helper')
-const { Aedes } = require('../aedes.js')
+import { test } from 'tap'
+import { setup, connect, subscribe } from './helper.js'
+import { Aedes } from '../aedes.js'
 
 for (const qos of [0, 1, 2]) {
   const packet = {
diff --git a/test/client-pub-sub.js b/test/client-pub-sub.js
index e95e778..bf0eb74 100644
--- a/test/client-pub-sub.js
+++ b/test/client-pub-sub.js
@@ -1,8 +1,6 @@
-'use strict'
-
-const { test } = require('tap')
-const { setup, connect, subscribe, noError } = require('./helper')
-const { Aedes } = require('../')
+import { test } from 'tap'
+import { setup, connect, subscribe, noError } from './helper.js'
+import { Aedes } from '../aedes.js'
 
 test('publish direct to a single client QoS 0', function (t) {
   t.plan(2)
diff --git a/test/close_socket_by_other_party.js b/test/close_socket_by_other_party.js
index 6b5bcd4..79bd85e 100644
--- a/test/close_socket_by_other_party.js
+++ b/test/close_socket_by_other_party.js
@@ -1,9 +1,9 @@
-'use strict'
-
-const { test } = require('tap')
-const EventEmitter = require('events')
-const { setup, connect, subscribe } = require('./helper')
-const { Aedes } = require('../')
+import { test } from 'tap'
+import EventEmitter from 'node:events'
+import { setup, connect, subscribe } from './helper.js'
+import { Aedes } from '../aedes.js'
+import mqtt from 'mqtt'
+import { createServer } from 'node:net'
 
 test('aedes is closed before client authenticate returns', function (t) {
   t.plan(1)
@@ -127,8 +127,6 @@ test('close client when its socket is closed', function (t) {
 
 test('multiple clients subscribe same topic, and all clients still receive message except the closed one', function (t) {
   t.plan(5)
-
-  const mqtt = require('mqtt')
   Aedes.createBroker().then((broker) => {
     let client2
 
@@ -138,7 +136,7 @@ test('multiple clients subscribe same topic, and all clients still receive messa
       server.close()
     })
 
-    const server = require('net').createServer(broker.handle)
+    const server = createServer(broker.handle)
     const port = 1883
     server.listen(port)
     broker.on('clientError', function (client, err) {
diff --git a/test/connect.js b/test/connect.js
index 03cd481..9c98dbc 100644
--- a/test/connect.js
+++ b/test/connect.js
@@ -1,11 +1,11 @@
-'use strict'
-
-const { test } = require('tap')
-const http = require('http')
-const ws = require('ws')
-const mqtt = require('mqtt')
-const { setup, connect, delay } = require('./helper')
-const { Aedes } = require('../')
+import { test } from 'tap'
+import http from 'node:http'
+import { WebSocketServer, createWebSocketStream } from 'ws'
+import mqtt from 'mqtt'
+import { setup, connect, delay } from './helper.js'
+import { Aedes } from '../aedes.js'
+import handleConnect from '../lib/handlers/connect.js'
+import handle from '../lib/handlers/index.js'
 
 ;[{ ver: 3, id: 'MQIsdp' }, { ver: 4, id: 'MQTT' }].forEach(function (ele) {
   test('connect and connack (minimal)', function (t) {
@@ -492,9 +492,6 @@ test('connect handler calls done when preConnect throws error', function (t) {
     t.teardown(broker.close.bind(broker))
 
     const s = setup(broker)
-
-    const handleConnect = require('../lib/handlers/connect')
-
     handleConnect(s.client, {}, function done (err) {
       t.equal(err.message, 'error in preconnect', 'calls done with error')
     })
@@ -509,8 +506,6 @@ test('handler calls done when disconnect or unknown packet cmd is received', fun
 
     const s = setup(broker)
 
-    const handle = require('../lib/handlers/index')
-
     handle(s.client, { cmd: 'disconnect' }, function done () {
       t.pass('calls done when disconnect cmd is received')
     })
@@ -762,12 +757,12 @@ test('websocket clients have access to the request object', function (t) {
     })
 
     const server = http.createServer()
-    const wss = new ws.WebSocketServer({
+    const wss = new WebSocketServer({
       server
     })
     wss.on('connection', (websocket, req) => {
       // websocket is a WebSocket, but aedes expects a stream.
-      const stream = ws.createWebSocketStream(websocket)
+      const stream = createWebSocketStream(websocket)
       broker.handle(stream, req)
     })
 
diff --git a/test/events.js b/test/events.js
index a1e4865..b9fb60f 100644
--- a/test/events.js
+++ b/test/events.js
@@ -1,9 +1,9 @@
-'use strict'
-
-const { test } = require('tap')
-const mqemitter = require('mqemitter')
-const { setup, connect, subscribe } = require('./helper')
-const { Aedes } = require('../')
+import { test } from 'tap'
+import mqemitter from 'mqemitter'
+import { setup, connect, subscribe } from './helper.js'
+import { Aedes } from '../aedes.js'
+import mqtt from 'mqtt'
+import { createServer } from 'node:net'
 
 test('publishes an hearbeat', function (t) {
   t.plan(2)
@@ -203,8 +203,7 @@ test('Test backpressure aedes published function', function (t) {
       } else { done() }
     }
   }).then((broker) => {
-    const mqtt = require('mqtt')
-    const server = require('net').createServer(broker.handle)
+    const server = createServer(broker.handle)
 
     server.listen(0, function () {
       const port = server.address().port
diff --git a/test/helper.js b/test/helper.js
index e4449aa..8017c81 100644
--- a/test/helper.js
+++ b/test/helper.js
@@ -1,15 +1,13 @@
-'use strict'
-
-const duplexify = require('duplexify')
-const mqtt = require('mqtt-connection')
-const { through } = require('../lib/utils')
-const util = require('util')
+import duplexify from 'duplexify'
+import mqtt from 'mqtt-connection'
+import { through } from '../lib/utils.js'
+import util from 'util'
 
 const parseStream = mqtt.parseStream
 const generateStream = mqtt.generateStream
 let clients = 0
 
-function setup (broker) {
+export function setup (broker) {
   const inStream = generateStream()
   const outStream = parseStream()
   const conn = duplexify(outStream, inStream)
@@ -23,7 +21,7 @@ function setup (broker) {
   }
 }
 
-function connect (s, opts, connected) {
+export function connect (s, opts, connected) {
   s = Object.create(s)
   s.outStream = s.outStream.pipe(through(filter))
 
@@ -57,7 +55,7 @@ function connect (s, opts, connected) {
   }
 }
 
-function noError (s, t) {
+export function noError (s, t) {
   s.broker.on('clientError', function (client, err) {
     if (err) throw err
     t.notOk(err, 'must not error')
@@ -66,7 +64,7 @@ function noError (s, t) {
   return s
 }
 
-function subscribe (t, subscriber, topic, qos, done) {
+export function subscribe (t, subscriber, topic, qos, done) {
   subscriber.inStream.write({
     cmd: 'subscribe',
     messageId: 24,
@@ -88,7 +86,7 @@ function subscribe (t, subscriber, topic, qos, done) {
 }
 
 // subs: [{topic:, qos:}]
-function subscribeMultiple (t, subscriber, subs, expectedGranted, done) {
+export function subscribeMultiple (t, subscriber, subs, expectedGranted, done) {
   subscriber.inStream.write({
     cmd: 'subscribe',
     messageId: 24,
@@ -106,11 +104,4 @@ function subscribeMultiple (t, subscriber, subs, expectedGranted, done) {
   })
 }
 
-module.exports = {
-  setup,
-  connect,
-  noError,
-  subscribe,
-  subscribeMultiple,
-  delay: util.promisify(setTimeout)
-}
+export const delay = util.promisify(setTimeout)
diff --git a/test/keep-alive.js b/test/keep-alive.js
index cc402ee..cb303a7 100644
--- a/test/keep-alive.js
+++ b/test/keep-alive.js
@@ -1,10 +1,8 @@
-'use strict'
-
-const { test } = require('tap')
-const eos = require('end-of-stream')
-const Faketimers = require('@sinonjs/fake-timers')
-const { setup, connect, noError } = require('./helper')
-const { Aedes } = require('../')
+import { test } from 'tap'
+import eos from 'end-of-stream'
+import Faketimers from '@sinonjs/fake-timers'
+import { setup, connect, noError } from './helper.js'
+import { Aedes } from '../aedes.js'
 
 test('supports pingreq/pingresp', function (t) {
   t.plan(1)
diff --git a/test/meta.js b/test/meta.js
index d3ec65c..c621deb 100644
--- a/test/meta.js
+++ b/test/meta.js
@@ -1,8 +1,8 @@
-'use strict'
-
-const { test } = require('tap')
-const { setup, connect, subscribe, noError } = require('./helper')
-const { Aedes } = require('../')
+import { test } from 'tap'
+import { setup, connect, subscribe, noError } from './helper.js'
+import { Aedes } from '../aedes.js'
+import pkg from '../package.json' with { type: 'json' }
+const version = pkg.version
 
 test('count connected clients', function (t) {
   t.plan(4)
@@ -298,7 +298,7 @@ test('get aedes version', function (t) {
   Aedes.createBroker().then((broker) => {
     t.teardown(broker.close.bind(broker))
 
-    t.equal(broker.version, require('../package.json').version)
+    t.equal(broker.version, version)
   })
 })
 
diff --git a/test/not-blocking.js b/test/not-blocking.js
index d54b38e..41f7c90 100644
--- a/test/not-blocking.js
+++ b/test/not-blocking.js
@@ -1,11 +1,9 @@
-'use strict'
-
-const { test } = require('tap')
-const EventEmitter = require('events')
-const mqtt = require('mqtt')
-const net = require('net')
-const Faketimers = require('@sinonjs/fake-timers')
-const { Aedes } = require('../')
+import { test } from 'tap'
+import EventEmitter from 'node:events'
+import mqtt from 'mqtt'
+import net from 'net'
+import Faketimers from '@sinonjs/fake-timers'
+import { Aedes } from '../aedes.js'
 
 test('connect 500 concurrent clients', function (t) {
   t.plan(3)
diff --git a/test/qos1.js b/test/qos1.js
index 1f045d9..add66d8 100644
--- a/test/qos1.js
+++ b/test/qos1.js
@@ -1,10 +1,10 @@
-'use strict'
+import { test } from 'tap'
+import concat from 'concat-stream'
+import { setup, connect, subscribe } from './helper.js'
+import Faketimers from '@sinonjs/fake-timers'
+import { Aedes } from '../aedes.js'
+import { through } from '../lib/utils.js'
 
-const { test } = require('tap')
-const concat = require('concat-stream')
-const { setup, connect, subscribe } = require('./helper')
-const Faketimers = require('@sinonjs/fake-timers')
-const { Aedes } = require('../')
 const noop = () => {}
 
 test('publish QoS 1', function (t) {
@@ -540,7 +540,6 @@ test('resend many publish on non-clean reconnect QoS 1', function (t) {
     const opts = { clean: false, clientId: 'abcde' }
     let subscriber = connect(setup(broker), opts)
     const publisher = connect(setup(broker))
-    const { through } = require('../lib/utils')
     const total = through().writableHighWaterMark * 2
 
     let received = 0
diff --git a/test/qos2.js b/test/qos2.js
index 9bead08..328d40c 100644
--- a/test/qos2.js
+++ b/test/qos2.js
@@ -1,9 +1,8 @@
-'use strict'
-
-const { test } = require('tap')
-const concat = require('concat-stream')
-const { setup, connect, subscribe } = require('./helper')
-const { Aedes } = require('../')
+import { test } from 'tap'
+import concat from 'concat-stream'
+import { setup, connect, subscribe } from './helper.js'
+import { Aedes } from '../aedes.js'
+import handle from '../lib/handlers/pubrec.js'
 
 function publish (t, s, packet, done) {
   const msgId = packet.messageId
@@ -155,8 +154,6 @@ test('pubrec handler calls done when outgoingUpdate fails (clean=false)', functi
     const s = connect(setup(broker), { clean: false })
     t.teardown(s.broker.close.bind(s.broker))
 
-    const handle = require('../lib/handlers/pubrec.js')
-
     s.broker.persistence.outgoingUpdate = async () => {
       throw Error('throws error')
     }
diff --git a/test/regr-21.js b/test/regr-21.js
index 71a0d58..fb014fb 100644
--- a/test/regr-21.js
+++ b/test/regr-21.js
@@ -1,8 +1,6 @@
-'use strict'
-
-const { test } = require('tap')
-const { setup, connect } = require('./helper')
-const { Aedes } = require('../')
+import { test } from 'tap'
+import { setup, connect } from './helper.js'
+import { Aedes } from '../aedes.js'
 
 test('after an error, outstanding packets are discarded', function (t) {
   t.plan(1)
diff --git a/test/require.cjs b/test/require.cjs
new file mode 100644
index 0000000..fd8c6e6
--- /dev/null
+++ b/test/require.cjs
@@ -0,0 +1,24 @@
+// a test to see if CJS require still works
+
+const { Aedes } = require('../aedes.js')
+const defaultExport = require('../aedes.js')
+const { test } = require('tap')
+
+test('test Aedes constructor', function (t) {
+  t.plan(1)
+  const aedes = new Aedes()
+  t.equal(aedes instanceof Aedes, true, 'Aedes constructor works')
+})
+
+test('test warning on default export', function (t) {
+  t.plan(1)
+  t.throws(defaultExport, 'received expected error')
+})
+
+test('test aedes.createBroker', (t) => {
+  t.plan(1)
+  Aedes.createBroker().then((broker) => {
+    t.teardown(broker.close.bind(broker))
+    t.pass('Aedes.createBroker works')
+  })
+})
diff --git a/test/retain.js b/test/retain.js
index ab3b992..3d0fccf 100644
--- a/test/retain.js
+++ b/test/retain.js
@@ -1,9 +1,7 @@
-'use strict'
-
-const { test } = require('tap')
-const Faketimers = require('@sinonjs/fake-timers')
-const { setup, connect, subscribe, noError } = require('./helper')
-const { Aedes } = require('../')
+import { test } from 'tap'
+import Faketimers from '@sinonjs/fake-timers'
+import { setup, connect, subscribe, noError } from './helper.js'
+import { Aedes } from '../aedes.js'
 
 // [MQTT-3.3.1-9]
 test('live retain packets', function (t) {
diff --git a/test/topics.js b/test/topics.js
index af9d5b8..77889b9 100644
--- a/test/topics.js
+++ b/test/topics.js
@@ -1,9 +1,7 @@
-'use strict'
-
-const { test } = require('tap')
-const { setup, connect, subscribe } = require('./helper')
-const { Aedes } = require('../')
-const { validateTopic } = require('../lib/utils')
+import { test } from 'tap'
+import { setup, connect, subscribe } from './helper.js'
+import { Aedes } from '../aedes.js'
+import { validateTopic } from '../lib/utils.js'
 
 test('validation of `null` topic', function (t) {
   // issue #780
diff --git a/test/types/aedes.test-d.ts b/test/types/aedes.test-d.ts
index 59444b5..6441841 100644
--- a/test/types/aedes.test-d.ts
+++ b/test/types/aedes.test-d.ts
@@ -5,8 +5,8 @@ import type {
   AuthenticateError,
   Client,
   Connection
-} from '../../aedes'
-import { Aedes } from '../../aedes'
+} from '../../aedes.js'
+import { Aedes } from '../../aedes.js'
 import type { AedesPublishPacket, ConnackPacket, ConnectPacket, PingreqPacket, PublishPacket, PubrelPacket, Subscription, SubscribePacket, UnsubscribePacket } from '../../types/packet'
 import { expectType } from 'tsd'
 
diff --git a/test/will.js b/test/will.js
index d0dedbe..5c1fcee 100644
--- a/test/will.js
+++ b/test/will.js
@@ -1,10 +1,8 @@
-'use strict'
-
-const { test } = require('tap')
-const memory = require('aedes-persistence')
-const Faketimers = require('@sinonjs/fake-timers')
-const { setup, connect, noError, delay } = require('./helper')
-const { Aedes } = require('../')
+import { test } from 'tap'
+import memory from 'aedes-persistence'
+import Faketimers from '@sinonjs/fake-timers'
+import { setup, connect, noError, delay } from './helper.js'
+import { Aedes } from '../aedes.js'
 
 async function memorySetup (opts) {
   const persistence = memory()