96dc79cfe6
- 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>
356 lines
12 KiB
JavaScript
356 lines
12 KiB
JavaScript
#!/usr/bin/env node
|
|
|
|
// One-shot CJS → ESM conversion script for the box codebase.
|
|
// Usage:
|
|
// node scripts/cjs-to-esm.mjs # convert all src/ + syslog.js
|
|
// node scripts/cjs-to-esm.mjs --dry-run # preview without writing
|
|
// node scripts/cjs-to-esm.mjs src/paths.js # convert specific file(s)
|
|
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
|
|
const ROOT = process.cwd();
|
|
const DRY_RUN = process.argv.includes('--dry-run');
|
|
const args = process.argv.slice(2).filter(a => !a.startsWith('--'));
|
|
|
|
// ========== File Collection ==========
|
|
|
|
function walk(dir) {
|
|
const result = [];
|
|
for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
const p = path.join(dir, e.name);
|
|
if (e.isDirectory()) result.push(...walk(p));
|
|
else if (e.name.endsWith('.js')) result.push(p);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function getFiles() {
|
|
if (args.length) return args.map(f => path.resolve(f));
|
|
const files = walk(path.join(ROOT, 'src'));
|
|
files.push(path.join(ROOT, 'syslog.js'));
|
|
return files;
|
|
}
|
|
|
|
// ========== NPM Package ESM Detection ==========
|
|
|
|
const esmCache = {};
|
|
function isEsmPkg(modPath) {
|
|
const name = modPath.startsWith('@') ? modPath.split('/').slice(0, 2).join('/') : modPath.split('/')[0];
|
|
if (name in esmCache) return esmCache[name];
|
|
try {
|
|
const pkg = JSON.parse(fs.readFileSync(path.join(ROOT, 'node_modules', name, 'package.json'), 'utf-8'));
|
|
esmCache[name] = pkg.type === 'module';
|
|
} catch {
|
|
esmCache[name] = false;
|
|
}
|
|
return esmCache[name];
|
|
}
|
|
|
|
// ========== Import Path Fixup ==========
|
|
|
|
function fixPath(mod, fromFile) {
|
|
if (!mod.startsWith('.')) return mod;
|
|
if (mod.endsWith('.js') || mod.endsWith('.mjs')) return mod;
|
|
const dir = path.dirname(fromFile);
|
|
const resolved = path.resolve(dir, mod);
|
|
if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) return mod + '/index.js';
|
|
if (fs.existsSync(resolved + '.js')) return mod + '.js';
|
|
return mod;
|
|
}
|
|
|
|
// ========== Convert a Single Require Declaration ==========
|
|
// Input: trimmed declaration text WITHOUT leading const/let/var and trailing ,;
|
|
// Returns { imports: string[], consts: string[] } or null
|
|
|
|
function convertDecl(decl, fromFile) {
|
|
decl = decl.trim().replace(/[,;]\s*$/, '').trim();
|
|
if (!decl || !decl.includes('require(')) return null;
|
|
|
|
let m;
|
|
|
|
// 1) name = require('mod')('args...') e.g. debug = require('debug')('box:dns')
|
|
m = decl.match(/^(\w+)\s*=\s*require\(['"]([^'"]+)['"]\)\((.+)\)$/);
|
|
if (m) {
|
|
const [, name, mod, callArgs] = m;
|
|
const alias = name + 'Module';
|
|
return {
|
|
imports: [`import ${alias} from '${fixPath(mod, fromFile)}';`],
|
|
consts: [`const ${name} = ${alias}(${callArgs});`]
|
|
};
|
|
}
|
|
|
|
// 2) name = require('mod').prop e.g. HttpError = require('@cloudron/connect-lastmile').HttpError
|
|
m = decl.match(/^(\w+)\s*=\s*require\(['"]([^'"]+)['"]\)\.(\w+)$/);
|
|
if (m) {
|
|
const [, name, mod, prop] = m;
|
|
const fp = fixPath(mod, fromFile);
|
|
const imp = name === prop
|
|
? `import { ${prop} } from '${fp}';`
|
|
: `import { ${prop} as ${name} } from '${fp}';`;
|
|
return { imports: [imp], consts: [] };
|
|
}
|
|
|
|
// 3) { a, b } = require('mod') e.g. { CronTime } = require('cron')
|
|
m = decl.match(/^(\{[^}]+\})\s*=\s*require\(['"]([^'"]+)['"]\)$/);
|
|
if (m) {
|
|
const [, destr, mod] = m;
|
|
return { imports: [`import ${destr} from '${fixPath(mod, fromFile)}';`], consts: [] };
|
|
}
|
|
|
|
// 4) name = require('mod') e.g. apps = require('./apps.js')
|
|
m = decl.match(/^(\w+)\s*=\s*require\(['"]([^'"]+)['"]\)$/);
|
|
if (m) {
|
|
const [, name, mod] = m;
|
|
const fp = fixPath(mod, fromFile);
|
|
// Use namespace import for ESM-only npm packages (they likely have no default export)
|
|
const useNamespace = !mod.startsWith('.') && !mod.startsWith('node:') && isEsmPkg(mod);
|
|
const imp = useNamespace
|
|
? `import * as ${name} from '${fp}';`
|
|
: `import ${name} from '${fp}';`;
|
|
return { imports: [imp], consts: [] };
|
|
}
|
|
|
|
return null; // unknown pattern
|
|
}
|
|
|
|
// ========== Process a const-require Block ==========
|
|
|
|
function handleBlock(blockLines, imports, consts, warnings, fromFile) {
|
|
const block = blockLines.join('\n');
|
|
// Strip leading const/let/var and trailing ;
|
|
let inner = block.replace(/^\s*(const|let|var)\s+/, '');
|
|
inner = inner.replace(/;\s*$/, '');
|
|
|
|
// Split individual declarations by comma-at-end-of-line
|
|
const decls = inner.split(/,\n/).map(d => d.trim());
|
|
|
|
for (const decl of decls) {
|
|
if (!decl) continue;
|
|
|
|
if (!decl.includes('require(')) {
|
|
// Non-require declaration in a mixed block — keep as const
|
|
consts.push(`const ${decl};`);
|
|
continue;
|
|
}
|
|
|
|
const result = convertDecl(decl, fromFile);
|
|
if (result) {
|
|
imports.push(...result.imports);
|
|
if (result.consts.length) consts.push(...result.consts);
|
|
} else {
|
|
warnings.push(`Unknown require pattern: ${decl}`);
|
|
consts.push(`const ${decl};`);
|
|
}
|
|
}
|
|
}
|
|
|
|
// ========== Convert Exports Block ==========
|
|
|
|
function convertExports(block) {
|
|
// exports = module.exports = { ... };
|
|
let m = block.match(/exports\s*=\s*module\.exports\s*=\s*(\{[\s\S]*\});/);
|
|
if (m) return `export default ${m[1]};`;
|
|
|
|
// exports = module.exports = name;
|
|
m = block.match(/exports\s*=\s*module\.exports\s*=\s*(\w+)\s*;/);
|
|
if (m) return `export default ${m[1]};`;
|
|
|
|
return block; // fallback
|
|
}
|
|
|
|
// ========== Main File Processor ==========
|
|
|
|
function processFile(filePath) {
|
|
const lines = fs.readFileSync(filePath, 'utf-8').split('\n');
|
|
const imports = [];
|
|
const consts = [];
|
|
const output = [];
|
|
const warnings = [];
|
|
|
|
let i = 0;
|
|
let inBlock = false;
|
|
let blockLines = [];
|
|
|
|
while (i < lines.length) {
|
|
const line = lines[i];
|
|
const t = line.trim();
|
|
|
|
// --- Skip 'use strict' ---
|
|
if (t === "'use strict';" || t === '"use strict";') {
|
|
i++;
|
|
if (i < lines.length && lines[i].trim() === '') i++;
|
|
continue;
|
|
}
|
|
|
|
// --- Detect start of const/let/var block ---
|
|
if (!inBlock && /^\s*(const|let|var)\s+/.test(line)) {
|
|
blockLines = [line];
|
|
if (t.endsWith(';')) {
|
|
// Single-line block
|
|
if (/require\(/.test(line)) {
|
|
handleBlock(blockLines, imports, consts, warnings, filePath);
|
|
} else {
|
|
output.push(line);
|
|
}
|
|
} else {
|
|
inBlock = true;
|
|
}
|
|
i++;
|
|
continue;
|
|
}
|
|
|
|
// --- Continue multi-line const block ---
|
|
if (inBlock) {
|
|
blockLines.push(line);
|
|
if (t.endsWith(';')) {
|
|
const fullBlock = blockLines.join('\n');
|
|
if (/require\(/.test(fullBlock)) {
|
|
handleBlock(blockLines, imports, consts, warnings, filePath);
|
|
} else {
|
|
output.push(...blockLines);
|
|
}
|
|
inBlock = false;
|
|
}
|
|
i++;
|
|
continue;
|
|
}
|
|
|
|
// --- Exports block ---
|
|
if (/^\s*exports\s*=\s*module\.exports\s*=/.test(line)) {
|
|
let block = line;
|
|
if (!t.endsWith(';')) {
|
|
i++;
|
|
while (i < lines.length) {
|
|
block += '\n' + lines[i];
|
|
if (lines[i].trim().endsWith(';')) { i++; break; }
|
|
i++;
|
|
}
|
|
} else {
|
|
i++;
|
|
}
|
|
|
|
if (/require\(/.test(block)) {
|
|
warnings.push('Inline require() in exports block — needs manual fix');
|
|
output.push(block);
|
|
} else {
|
|
output.push(convertExports(block));
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// --- require.main === module ---
|
|
if (/require\.main\s*===\s*module/.test(t)) {
|
|
output.push(line.replace(/require\.main\s*===\s*module/, 'process.argv[1] === import.meta.filename'));
|
|
i++;
|
|
continue;
|
|
}
|
|
|
|
// --- Warn about any remaining require() calls ---
|
|
if (/\brequire\s*\(/.test(t) && !/^\s*\/\//.test(line) && !/^\s*\*/.test(line)) {
|
|
warnings.push(`Line ${i + 1}: Unconverted require(): ${t}`);
|
|
}
|
|
|
|
output.push(line);
|
|
i++;
|
|
}
|
|
|
|
// --- Replace __dirname / __filename with import.meta equivalents (Node 21.2+) ---
|
|
for (let j = 0; j < output.length; j++) {
|
|
output[j] = output[j].replace(/\b__dirname\b/g, 'import.meta.dirname');
|
|
output[j] = output[j].replace(/\b__filename\b/g, 'import.meta.filename');
|
|
}
|
|
|
|
// --- Reconstruct file ---
|
|
// Preserve shebang and leading block comments before imports
|
|
const leading = [];
|
|
let start = 0;
|
|
|
|
// Shebang
|
|
if (output.length > 0 && output[0].trimStart().startsWith('#!')) {
|
|
leading.push(output[0]);
|
|
start = 1;
|
|
// skip blank line after shebang
|
|
while (start < output.length && output[start].trim() === '') start++;
|
|
}
|
|
|
|
// Leading block comment (/* jslint */, /* global */, /* eslint */)
|
|
if (start < output.length && output[start].trim().startsWith('/*')) {
|
|
// Could be single-line or multi-line
|
|
if (output[start].includes('*/')) {
|
|
leading.push(output[start]);
|
|
start++;
|
|
} else {
|
|
while (start < output.length) {
|
|
leading.push(output[start]);
|
|
if (output[start].includes('*/')) { start++; break; }
|
|
start++;
|
|
}
|
|
}
|
|
// skip blank line after comment
|
|
if (start < output.length && output[start].trim() === '') start++;
|
|
}
|
|
|
|
const rest = output.slice(start);
|
|
// Remove leading blank lines from rest
|
|
while (rest.length > 0 && rest[0].trim() === '') rest.shift();
|
|
|
|
// Build final content
|
|
const parts = [];
|
|
if (leading.length > 0) {
|
|
parts.push(...leading);
|
|
parts.push('');
|
|
}
|
|
if (imports.length > 0) {
|
|
parts.push(...imports);
|
|
}
|
|
if (consts.length > 0) {
|
|
if (imports.length > 0) parts.push('');
|
|
parts.push(...consts);
|
|
}
|
|
if (imports.length > 0 || consts.length > 0) {
|
|
parts.push('');
|
|
}
|
|
parts.push(...rest);
|
|
|
|
let result = parts.join('\n');
|
|
// Clean up 3+ consecutive blank lines → 2
|
|
result = result.replace(/\n{3,}/g, '\n\n');
|
|
// Ensure trailing newline
|
|
if (!result.endsWith('\n')) result += '\n';
|
|
|
|
return { content: result, warnings };
|
|
}
|
|
|
|
// ========== Run ==========
|
|
|
|
const files = getFiles();
|
|
console.log(`Processing ${files.length} files${DRY_RUN ? ' (dry run)' : ''}...\n`);
|
|
|
|
let totalWarnings = 0;
|
|
for (const file of files) {
|
|
const rel = path.relative(ROOT, file);
|
|
try {
|
|
const { content, warnings } = processFile(file);
|
|
|
|
if (warnings.length > 0) {
|
|
console.log(`⚠ ${rel}:`);
|
|
for (const w of warnings) console.log(` ${w}`);
|
|
totalWarnings += warnings.length;
|
|
}
|
|
|
|
if (!DRY_RUN) {
|
|
fs.writeFileSync(file, content);
|
|
console.log(`✓ ${rel}`);
|
|
} else {
|
|
console.log(` ${rel}${warnings.length ? '' : ' (ok)'}`);
|
|
}
|
|
} catch (err) {
|
|
console.error(`✗ ${rel}: ${err.message}`);
|
|
totalWarnings++;
|
|
}
|
|
}
|
|
|
|
console.log(`\nDone. ${files.length} files processed, ${totalWarnings} warnings.`);
|
|
if (totalWarnings > 0) console.log('Review warnings above and fix manually.');
|