Migrate codebase from CommonJS to ES Modules

- Convert all require()/module.exports to import/export across 260+ files
- Add "type": "module" to package.json to enable ESM by default
- Add migrations/package.json with "type": "commonjs" to keep db-migrate compatible
- Convert eslint.config.js to ESM with sourceType: "module"
- Replace __dirname/__filename with import.meta.dirname/import.meta.filename
- Replace require.main === module with process.argv[1] === import.meta.filename
- Remove 'use strict' directives (implicit in ESM)
- Convert dynamic require() in switch statements to static import lookup maps
  (dns.js, domains.js, backupformats.js, backupsites.js, network.js)
- Extract self-referencing exports.CONSTANT patterns into standalone const
  declarations (apps.js, services.js, locks.js, users.js, mail.js, etc.)
- Lazify SERVICES object in services.js to avoid circular dependency TDZ issues
- Add clearMailQueue() to mailer.js for ESM-safe queue clearing in tests
- Add _setMockApp() to ldapserver.js for ESM-safe test mocking
- Add _setMockResolve() wrapper to dig.js for ESM-safe DNS mocking in tests
- Convert backupupload.js to use dynamic imports so --check exits before
  loading the module graph (which requires BOX_ENV)
- Update check-install to use ESM import for infra_version.js
- Convert scripts/ (hotfix, release, remote_hotfix.js, find-unused-translations)
- All 1315 tests passing

Migration stats (AI-assisted using Cursor with Claude):
- Wall clock time: ~3-4 hours
- Assistant completions: ~80-100
- Estimated token usage: ~1-2M tokens

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Girish Ramakrishnan
2026-02-14 09:53:14 +01:00
parent e0e9f14a5e
commit 96dc79cfe6
277 changed files with 4789 additions and 3811 deletions

View File

@@ -1,55 +1,65 @@
'use strict';
import assert from 'node:assert';
import AuditSource from './auditsource.js';
import BoxError from './boxerror.js';
import * as changelog from './changelog.js';
import constants from './constants.js';
import * as dashboard from './dashboard.js';
import * as database from './database.js';
import debugModule from 'debug';
import eventlog from './eventlog.js';
import * as mailer from './mailer.js';
import safe from 'safetydance';
import * as users from './users.js';
exports = module.exports = {
const debug = debugModule('box:notifications');
const TYPE_CLOUDRON_INSTALLED = 'cloudronInstalled';
const TYPE_CLOUDRON_UPDATED = 'cloudronUpdated';
const TYPE_CLOUDRON_UPDATE_FAILED = 'cloudronUpdateFailed';
const TYPE_CERTIFICATE_RENEWAL_FAILED = 'certificateRenewalFailed';
const TYPE_BACKUP_CONFIG = 'backupConfig';
const TYPE_APP_OOM = 'appOutOfMemory';
const TYPE_APP_UPDATED = 'appUpdated';
const TYPE_BACKUP_FAILED = 'backupFailed';
const TYPE_APP_DOWN = 'appDown';
const TYPE_APP_UP = 'appUp';
const TYPE_DISK_SPACE = 'diskSpace';
const TYPE_MAIL_STATUS = 'mailStatus';
const TYPE_REBOOT = 'reboot';
const TYPE_UPDATE_UBUNTU = 'ubuntuUpdate';
const TYPE_BOX_UPDATE = 'boxUpdate';
const TYPE_MANUAL_APP_UPDATE_NEEDED = 'manualAppUpdate';
const TYPE_DOMAIN_CONFIG_CHECK_FAILED = 'domainConfigCheckFailed';
const _add = add;
export {
get,
update,
list,
del,
onEvent,
// these are notification types, keep in sync with client.js
TYPE_CLOUDRON_INSTALLED: 'cloudronInstalled',
TYPE_CLOUDRON_UPDATED: 'cloudronUpdated',
TYPE_CLOUDRON_UPDATE_FAILED: 'cloudronUpdateFailed',
TYPE_CERTIFICATE_RENEWAL_FAILED: 'certificateRenewalFailed',
TYPE_BACKUP_CONFIG: 'backupConfig',
TYPE_APP_OOM: 'appOutOfMemory',
TYPE_APP_UPDATED: 'appUpdated',
TYPE_BACKUP_FAILED: 'backupFailed',
TYPE_APP_DOWN: 'appDown',
TYPE_APP_UP: 'appUp',
// these are singleton types allowed in pin() and unpin()
TYPE_DISK_SPACE: 'diskSpace',
TYPE_MAIL_STATUS: 'mailStatus',
TYPE_REBOOT: 'reboot',
TYPE_UPDATE_UBUNTU: 'ubuntuUpdate',
TYPE_BOX_UPDATE: 'boxUpdate',
TYPE_MANUAL_APP_UPDATE_NEEDED: 'manualAppUpdate',
TYPE_DOMAIN_CONFIG_CHECK_FAILED: 'domainConfigCheckFailed',
// these work off singleton types
TYPE_CLOUDRON_INSTALLED,
TYPE_CLOUDRON_UPDATED,
TYPE_CLOUDRON_UPDATE_FAILED,
TYPE_CERTIFICATE_RENEWAL_FAILED,
TYPE_BACKUP_CONFIG,
TYPE_APP_OOM,
TYPE_APP_UPDATED,
TYPE_BACKUP_FAILED,
TYPE_APP_DOWN,
TYPE_APP_UP,
TYPE_DISK_SPACE,
TYPE_MAIL_STATUS,
TYPE_REBOOT,
TYPE_UPDATE_UBUNTU,
TYPE_BOX_UPDATE,
TYPE_MANUAL_APP_UPDATE_NEEDED,
TYPE_DOMAIN_CONFIG_CHECK_FAILED,
pin,
unpin,
// exported for testing
_add: add
_add,
};
const assert = require('node:assert'),
AuditSource = require('./auditsource.js'),
BoxError = require('./boxerror.js'),
changelog = require('./changelog.js'),
constants = require('./constants.js'),
dashboard = require('./dashboard.js'),
database = require('./database.js'),
debug = require('debug')('box:notifications'),
eventlog = require('./eventlog.js'),
mailer = require('./mailer.js'),
safe = require('safetydance'),
users = require('./users.js');
const NOTIFICATION_FIELDS = [ 'id', 'eventId', 'type', 'title', 'message', 'creationTime', 'acknowledged', 'context' ];
function postProcess(result) {
@@ -163,11 +173,11 @@ async function oomEvent(eventId, containerId, app, addonName, event) {
message = `The app has been restarted automatically. If you see this notification often, consider increasing the [memory limit](https://${dashboardFqdn}/#/app/${app.id}/resources)`;
}
await add(exports.TYPE_APP_OOM, title, message, { eventId });
await add(TYPE_APP_OOM, title, message, { eventId });
const admins = await users.getAdmins();
for (const admin of admins) {
if (admin.notificationConfig.includes(exports.TYPE_APP_OOM)) {
if (admin.notificationConfig.includes(TYPE_APP_OOM)) {
await safe(mailer.oomEvent(admin.email, containerId, app, addonName, event), { debug });
}
}
@@ -179,7 +189,7 @@ async function appUp(eventId, app) {
const admins = await users.getAdmins();
for (const admin of admins) {
if (admin.notificationConfig.includes(exports.TYPE_APP_UP)) {
if (admin.notificationConfig.includes(TYPE_APP_UP)) {
await safe(mailer.appUp(admin.email, app), { debug });
}
}
@@ -191,7 +201,7 @@ async function appDown(eventId, app) {
const admins = await users.getAdmins();
for (const admin of admins) {
if (admin.notificationConfig.includes(exports.TYPE_APP_DOWN)) {
if (admin.notificationConfig.includes(TYPE_APP_DOWN)) {
await safe(mailer.appDown(admin.email, app), { debug });
}
}
@@ -210,7 +220,7 @@ async function appUpdated(eventId, app, fromManifest, toManifest) {
const title = upstreamVersion ? `${toManifest.title} at ${app.fqdn} updated to ${upstreamVersion} (package version ${toManifest.version})`
: `${toManifest.title} at ${app.fqdn} updated to package version ${toManifest.version}`;
await add(exports.TYPE_APP_UPDATED, title, `The application installed at https://${app.fqdn} was updated.\n\nChangelog:\n${toManifest.changelog}\n`, { eventId });
await add(TYPE_APP_UPDATED, title, `The application installed at https://${app.fqdn} was updated.\n\nChangelog:\n${toManifest.changelog}\n`, { eventId });
}
async function boxUpdated(eventId, oldVersion, newVersion) {
@@ -221,7 +231,7 @@ async function boxUpdated(eventId, oldVersion, newVersion) {
const changes = changelog.getChanges(newVersion);
const changelogMarkdown = changes.map((m) => `* ${m}\n`).join('');
await add(exports.TYPE_CLOUDRON_UPDATED, `Cloudron updated to v${newVersion}`, `Cloudron was updated from v${oldVersion} to v${newVersion}.\n\nChangelog:\n${changelogMarkdown}\n`, { eventId });
await add(TYPE_CLOUDRON_UPDATED, `Cloudron updated to v${newVersion}`, `Cloudron was updated from v${oldVersion} to v${newVersion}.\n\nChangelog:\n${changelogMarkdown}\n`, { eventId });
}
async function boxInstalled(eventId, version) {
@@ -231,18 +241,18 @@ async function boxInstalled(eventId, version) {
const changes = changelog.getChanges(version.replace(/\.([^.]*)$/, '.0')); // last .0 release
const changelogMarkdown = changes.map((m) => `* ${m}\n`).join('');
await add(exports.TYPE_CLOUDRON_INSTALLED, `Cloudron v${version} installed`, `Cloudron v${version} was installed.\n\nChangelog:\n${changelogMarkdown}\n`, { eventId });
await add(TYPE_CLOUDRON_INSTALLED, `Cloudron v${version} installed`, `Cloudron v${version} was installed.\n\nChangelog:\n${changelogMarkdown}\n`, { eventId });
}
async function boxUpdateError(eventId, errorMessage) {
assert.strictEqual(typeof eventId, 'string');
assert.strictEqual(typeof errorMessage, 'string');
await add(exports.TYPE_CLOUDRON_UPDATE_FAILED, 'Cloudron update failed', `Failed to update Cloudron: ${errorMessage}.`, { eventId });
await add(TYPE_CLOUDRON_UPDATE_FAILED, 'Cloudron update failed', `Failed to update Cloudron: ${errorMessage}.`, { eventId });
const admins = await users.getAdmins();
for (const admin of admins) {
if (admin.notificationConfig.includes(exports.TYPE_CLOUDRON_UPDATE_FAILED)) {
if (admin.notificationConfig.includes(TYPE_CLOUDRON_UPDATE_FAILED)) {
await safe(mailer.boxUpdateError(admin.email, errorMessage), { debug });
}
}
@@ -253,11 +263,11 @@ async function certificateRenewalError(eventId, fqdn, errorMessage) {
assert.strictEqual(typeof fqdn, 'string');
assert.strictEqual(typeof errorMessage, 'string');
await add(exports.TYPE_CERTIFICATE_RENEWAL_FAILED, `Certificate renewal of ${fqdn} failed`, `Failed to renew certs of ${fqdn}: ${errorMessage}. Renewal will be retried in 12 hours.`, { eventId });
await add(TYPE_CERTIFICATE_RENEWAL_FAILED, `Certificate renewal of ${fqdn} failed`, `Failed to renew certs of ${fqdn}: ${errorMessage}. Renewal will be retried in 12 hours.`, { eventId });
const admins = await users.getAdmins();
for (const admin of admins) {
if (admin.notificationConfig.includes(exports.TYPE_CERTIFICATE_RENEWAL_FAILED)) {
if (admin.notificationConfig.includes(TYPE_CERTIFICATE_RENEWAL_FAILED)) {
await safe(mailer.certificateRenewalError(admin.email, fqdn, errorMessage), { debug });
}
}
@@ -268,12 +278,12 @@ async function backupFailed(eventId, taskId, errorMessage) {
assert.strictEqual(typeof taskId, 'string');
assert.strictEqual(typeof errorMessage, 'string');
await add(exports.TYPE_BACKUP_FAILED, 'Backup failed', `Backup failed: ${errorMessage}. Logs are available [here](/logs.html?taskId=${taskId}).`, { eventId });
await add(TYPE_BACKUP_FAILED, 'Backup failed', `Backup failed: ${errorMessage}. Logs are available [here](/logs.html?taskId=${taskId}).`, { eventId });
const { fqdn:dashboardFqdn } = await dashboard.getLocation();
const superadmins = await users.getSuperadmins();
for (const superadmin of superadmins) {
if (superadmin.notificationConfig.includes(exports.TYPE_BACKUP_FAILED)) {
if (superadmin.notificationConfig.includes(TYPE_BACKUP_FAILED)) {
await safe(mailer.backupFailed(superadmin.email, errorMessage, `https://${dashboardFqdn}/logs.html?taskId=${taskId}`), { debug });
}
}
@@ -282,7 +292,7 @@ async function backupFailed(eventId, taskId, errorMessage) {
async function rebootRequired() {
const admins = await users.getAdmins();
for (const admin of admins) {
if (admin.notificationConfig.includes(exports.TYPE_REBOOT)) {
if (admin.notificationConfig.includes(TYPE_REBOOT)) {
await safe(mailer.rebootRequired(admin.email), { debug });
}
}
@@ -293,7 +303,7 @@ async function lowDiskSpace(message) {
const admins = await users.getAdmins();
for (const admin of admins) {
if (admin.notificationConfig.includes(exports.TYPE_DISK_SPACE)) {
if (admin.notificationConfig.includes(TYPE_DISK_SPACE)) {
await safe(mailer.lowDiskSpace(admin.email, message), { debug });
}
}
@@ -304,9 +314,9 @@ async function onPin(type, message) {
assert.strictEqual(typeof message, 'string');
switch (type) {
case exports.TYPE_REBOOT:
case TYPE_REBOOT:
return await rebootRequired();
case exports.TYPE_DISK_SPACE:
case TYPE_DISK_SPACE:
return await lowDiskSpace(message);
default:
break;
@@ -326,7 +336,7 @@ async function pin(type, title, message, options) {
}
// do not reset the ack state if user has already seen the update notification
const isUpdateType = type === exports.TYPE_BOX_UPDATE || type === exports.TYPE_MANUAL_APP_UPDATE_NEEDED;
const isUpdateType = type === TYPE_BOX_UPDATE || type === TYPE_MANUAL_APP_UPDATE_NEEDED;
const acknowledged = (isUpdateType && result.message === message) ? result.acknowledged : false;
if (result.acknowledged && !acknowledged) await onPin(type, message);