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:
@@ -0,0 +1,96 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// Check for import/export mismatches:
|
||||
// 1. Files using `import X from './file.js'` where file.js has no default export
|
||||
// 2. Files using `import * as X from './file.js'` where file.js ONLY has default export
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
const ROOT = process.cwd();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const srcFiles = walk(path.join(ROOT, 'src'));
|
||||
srcFiles.push(path.join(ROOT, 'syslog.js'));
|
||||
|
||||
// Build export map
|
||||
const exportMap = {}; // file -> 'default' | 'named' | 'both' | 'none'
|
||||
for (const file of srcFiles) {
|
||||
const content = fs.readFileSync(file, 'utf-8');
|
||||
const hasDefault = /^export default /m.test(content);
|
||||
const hasNamed = /^export \{/m.test(content) || /^export (const|let|var|function|class) /m.test(content);
|
||||
|
||||
if (hasDefault && hasNamed) exportMap[file] = 'both';
|
||||
else if (hasDefault) exportMap[file] = 'default';
|
||||
else if (hasNamed) exportMap[file] = 'named';
|
||||
else exportMap[file] = 'none';
|
||||
}
|
||||
|
||||
// Check imports
|
||||
let mismatches = 0;
|
||||
const fixes = []; // { file, line, from, to }
|
||||
|
||||
for (const file of srcFiles) {
|
||||
const content = fs.readFileSync(file, 'utf-8');
|
||||
const lines = content.split('\n');
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i];
|
||||
|
||||
// Default import of local file
|
||||
let m = line.match(/^import\s+(\w+)\s+from\s+'(\.[^']+\.js)';$/);
|
||||
if (m) {
|
||||
const [, name, importPath] = m;
|
||||
const resolved = path.resolve(path.dirname(file), importPath);
|
||||
const exportType = exportMap[resolved];
|
||||
if (exportType === 'named') {
|
||||
console.log(`MISMATCH: ${path.relative(ROOT, file)}:${i+1} - import ${name} from '${importPath}' but ${path.relative(ROOT, resolved)} has only named exports`);
|
||||
fixes.push({ file, lineNum: i, old: line, new: line.replace(`import ${name}`, `import * as ${name}`) });
|
||||
mismatches++;
|
||||
}
|
||||
}
|
||||
|
||||
// Namespace import of local file
|
||||
m = line.match(/^import\s+\*\s+as\s+(\w+)\s+from\s+'(\.[^']+\.js)';$/);
|
||||
if (m) {
|
||||
const [, name, importPath] = m;
|
||||
const resolved = path.resolve(path.dirname(file), importPath);
|
||||
const exportType = exportMap[resolved];
|
||||
if (exportType === 'default') {
|
||||
console.log(`MISMATCH: ${path.relative(ROOT, file)}:${i+1} - import * as ${name} from '${importPath}' but ${path.relative(ROOT, resolved)} has only default export`);
|
||||
fixes.push({ file, lineNum: i, old: line, new: line.replace(`import * as ${name}`, `import ${name}`) });
|
||||
mismatches++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\nFound ${mismatches} mismatches.`);
|
||||
|
||||
if (process.argv.includes('--fix') && fixes.length > 0) {
|
||||
// Group by file
|
||||
const byFile = {};
|
||||
for (const fix of fixes) {
|
||||
if (!byFile[fix.file]) byFile[fix.file] = [];
|
||||
byFile[fix.file].push(fix);
|
||||
}
|
||||
|
||||
for (const [file, fileFixes] of Object.entries(byFile)) {
|
||||
let content = fs.readFileSync(file, 'utf-8');
|
||||
for (const fix of fileFixes) {
|
||||
content = content.replace(fix.old, fix.new);
|
||||
}
|
||||
fs.writeFileSync(file, content);
|
||||
console.log(`Fixed: ${path.relative(ROOT, file)} (${fileFixes.length} changes)`);
|
||||
}
|
||||
console.log('All mismatches fixed.');
|
||||
}
|
||||
@@ -0,0 +1,355 @@
|
||||
#!/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.');
|
||||
@@ -0,0 +1,66 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// Fix consumers of files that still use export default { ... }
|
||||
// Change `import * as X from './file.js'` back to `import X from './file.js'`
|
||||
// for files whose target still uses export default.
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
const ROOT = process.cwd();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const srcFiles = walk(path.join(ROOT, 'src'));
|
||||
srcFiles.push(path.join(ROOT, 'syslog.js'));
|
||||
|
||||
// Find files that use export default (not named exports)
|
||||
const defaultExportFiles = new Set();
|
||||
for (const file of srcFiles) {
|
||||
const content = fs.readFileSync(file, 'utf-8');
|
||||
if (/^export default /m.test(content)) {
|
||||
defaultExportFiles.add(file);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Files with export default: ${defaultExportFiles.size}`);
|
||||
for (const f of defaultExportFiles) {
|
||||
console.log(` ${path.relative(ROOT, f)}`);
|
||||
}
|
||||
|
||||
// Fix consumers: change `import * as X from './file.js'` → `import X from './file.js'`
|
||||
// for files that use export default
|
||||
let changes = 0;
|
||||
|
||||
for (const file of srcFiles) {
|
||||
let content = fs.readFileSync(file, 'utf-8');
|
||||
let changed = false;
|
||||
|
||||
const importRegex = /^(import\s+)\* as (\w+)(\s+from\s+')(\.[^']+\.js)(';)$/gm;
|
||||
const newContent = content.replace(importRegex, (match, pre, name, mid, importPath, post) => {
|
||||
const dir = path.dirname(file);
|
||||
const resolved = path.resolve(dir, importPath);
|
||||
|
||||
if (defaultExportFiles.has(resolved)) {
|
||||
changed = true;
|
||||
changes++;
|
||||
return `${pre}${name}${mid}${importPath}${post}`;
|
||||
}
|
||||
return match;
|
||||
});
|
||||
|
||||
if (changed) {
|
||||
fs.writeFileSync(file, newContent);
|
||||
console.log(`✓ Fixed imports: ${path.relative(ROOT, file)}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\nReverted ${changes} imports to default import style.`);
|
||||
@@ -0,0 +1,177 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// Fix `exports.CONSTANT` self-references in ESM files.
|
||||
// For files with `export default { ... }`:
|
||||
// - Extract constant properties (simple key: 'literal' pairs) as standalone const declarations
|
||||
// - Replace the inline definition in the export block with shorthand property
|
||||
// - Replace `exports.CONSTANT` with `CONSTANT` everywhere
|
||||
// For files with `export { ... }` (constants already standalone):
|
||||
// - Just replace `exports.CONSTANT` with `CONSTANT`
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
const ROOT = path.resolve(import.meta.dirname, '..');
|
||||
const args = process.argv.slice(2).filter(a => !a.startsWith('--'));
|
||||
|
||||
function findFilesWithExportsRef(dir) {
|
||||
const results = [];
|
||||
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
||||
const full = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
if (entry.name === 'node_modules' || entry.name === 'migrations' || entry.name === 'dashboard') continue;
|
||||
results.push(...findFilesWithExportsRef(full));
|
||||
} else if (entry.name.endsWith('.js')) {
|
||||
const content = fs.readFileSync(full, 'utf8');
|
||||
if (/\bexports\./.test(content)) {
|
||||
results.push(full);
|
||||
}
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
function processFile(filePath) {
|
||||
let content = fs.readFileSync(filePath, 'utf8');
|
||||
const relPath = path.relative(ROOT, filePath);
|
||||
|
||||
// Find all exports.SOMETHING references
|
||||
const exportsRefs = new Set();
|
||||
const re = /\bexports\.(\w+)/g;
|
||||
let m;
|
||||
while ((m = re.exec(content)) !== null) {
|
||||
exportsRefs.add(m[1]);
|
||||
}
|
||||
|
||||
if (exportsRefs.size === 0) return;
|
||||
|
||||
console.log(`\n=== ${relPath} ===`);
|
||||
console.log(` Self-referenced constants: ${[...exportsRefs].join(', ')}`);
|
||||
|
||||
// Check if these constants already exist as standalone const/let/var declarations
|
||||
const alreadyDeclared = new Set();
|
||||
for (const name of exportsRefs) {
|
||||
const declPattern = new RegExp(`^(?:const|let|var)\\s+${name}\\s*=`, 'm');
|
||||
if (declPattern.test(content)) {
|
||||
alreadyDeclared.add(name);
|
||||
}
|
||||
}
|
||||
|
||||
const needsExtraction = [...exportsRefs].filter(n => !alreadyDeclared.has(n));
|
||||
|
||||
if (alreadyDeclared.size > 0) {
|
||||
console.log(` Already declared: ${[...alreadyDeclared].join(', ')}`);
|
||||
}
|
||||
|
||||
// For constants that need extraction, find them in the export default block
|
||||
if (needsExtraction.length > 0) {
|
||||
console.log(` Need extraction: ${needsExtraction.join(', ')}`);
|
||||
|
||||
// Find export default { ... } block
|
||||
// Parse it carefully - we need to handle the block properly
|
||||
const exportStart = content.indexOf('export default {');
|
||||
if (exportStart === -1) {
|
||||
console.log(` WARNING: No 'export default {' found but need extraction for: ${needsExtraction.join(', ')}`);
|
||||
// Just do the replacement anyway
|
||||
} else {
|
||||
// Find the matching closing brace
|
||||
let braceDepth = 0;
|
||||
let exportEnd = -1;
|
||||
let inString = false;
|
||||
let stringChar = '';
|
||||
for (let i = exportStart + 'export default '.length; i < content.length; i++) {
|
||||
const ch = content[i];
|
||||
if (inString) {
|
||||
if (ch === '\\') { i++; continue; }
|
||||
if (ch === stringChar) inString = false;
|
||||
continue;
|
||||
}
|
||||
if (ch === '\'' || ch === '"' || ch === '`') {
|
||||
inString = true;
|
||||
stringChar = ch;
|
||||
continue;
|
||||
}
|
||||
if (ch === '{') braceDepth++;
|
||||
else if (ch === '}') {
|
||||
braceDepth--;
|
||||
if (braceDepth === 0) {
|
||||
exportEnd = i + 1;
|
||||
// Include trailing semicolon
|
||||
if (content[exportEnd] === ';') exportEnd++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (exportEnd === -1) {
|
||||
console.log(` WARNING: Could not find end of export default block`);
|
||||
} else {
|
||||
const exportBlock = content.slice(exportStart, exportEnd);
|
||||
const lines = exportBlock.split('\n');
|
||||
|
||||
const extracted = []; // { name, value, originalLine }
|
||||
const newLines = [];
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
let found = false;
|
||||
|
||||
for (const name of needsExtraction) {
|
||||
// Match: NAME: 'value', or NAME: 123, or NAME: true,
|
||||
// Only match simple literal values (strings, numbers, booleans)
|
||||
const pattern = new RegExp(`^${name}:\\s*('[^']*'|"[^"]*"|\\d+(?:\\.\\d+)?|true|false|null),?\\s*(?:\\/\\/.*)?$`);
|
||||
const match = trimmed.match(pattern);
|
||||
if (match) {
|
||||
extracted.push({ name, value: match[1] });
|
||||
// Replace the line with just the shorthand property
|
||||
const indent = line.match(/^(\s*)/)[1];
|
||||
newLines.push(`${indent}${name},`);
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
newLines.push(line);
|
||||
}
|
||||
}
|
||||
|
||||
if (extracted.length > 0) {
|
||||
const constDecls = extracted.map(e => `const ${e.name} = ${e.value};`).join('\n');
|
||||
const newExportBlock = newLines.join('\n');
|
||||
content = content.slice(0, exportStart) + constDecls + '\n\n' + newExportBlock + content.slice(exportEnd);
|
||||
console.log(` Extracted ${extracted.length} constants: ${extracted.map(e => e.name).join(', ')}`);
|
||||
}
|
||||
|
||||
const notExtracted = needsExtraction.filter(n => !extracted.find(e => e.name === n));
|
||||
if (notExtracted.length > 0) {
|
||||
console.log(` WARNING: Could not extract (complex values?): ${notExtracted.join(', ')}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Replace all exports.CONSTANT references with just CONSTANT
|
||||
for (const name of exportsRefs) {
|
||||
const pattern = new RegExp(`\\bexports\\.${name}\\b`, 'g');
|
||||
content = content.replace(pattern, name);
|
||||
}
|
||||
|
||||
fs.writeFileSync(filePath, content);
|
||||
console.log(` Done!`);
|
||||
}
|
||||
|
||||
let files;
|
||||
if (args.length) {
|
||||
files = args.map(f => path.resolve(f));
|
||||
} else {
|
||||
files = findFilesWithExportsRef(path.join(ROOT, 'src'));
|
||||
}
|
||||
|
||||
console.log(`Processing ${files.length} files...`);
|
||||
|
||||
for (const file of files) {
|
||||
processFile(file);
|
||||
}
|
||||
|
||||
console.log('\nAll done!');
|
||||
@@ -0,0 +1,103 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// Phase 2: Convert export default { shorthand } → export { shorthand }
|
||||
// and update consumers from `import X from` → `import * as X from` for local files.
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
const ROOT = process.cwd();
|
||||
const DRY_RUN = process.argv.includes('--dry-run');
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const srcFiles = walk(path.join(ROOT, 'src'));
|
||||
srcFiles.push(path.join(ROOT, 'syslog.js'));
|
||||
|
||||
// Phase 1: Identify files with shorthand-only export default { ... }
|
||||
// and convert them to export { ... }
|
||||
const convertedFiles = new Set();
|
||||
|
||||
for (const file of srcFiles) {
|
||||
let content = fs.readFileSync(file, 'utf-8');
|
||||
|
||||
// Match export default { ... };
|
||||
const match = content.match(/^export default (\{[\s\S]*?\});/m);
|
||||
if (!match) continue;
|
||||
|
||||
const objContent = match[1];
|
||||
|
||||
// Check if this is shorthand-only (no "key: value" patterns)
|
||||
// Shorthand entries look like: " funcName," or " funcName" (last one)
|
||||
// Key-value entries look like: " KEY: value," or " key: value"
|
||||
// We strip the braces and check each line
|
||||
const inner = objContent.slice(1, -1); // remove { and }
|
||||
const lines = inner.split('\n').map(l => l.trim()).filter(l => l && !l.startsWith('//'));
|
||||
|
||||
// Check if any line has a colon that's NOT in a comment
|
||||
const hasKeyValue = lines.some(line => {
|
||||
// Remove trailing comma
|
||||
const cleaned = line.replace(/,\s*$/, '').trim();
|
||||
if (!cleaned) return false;
|
||||
// A shorthand property is just an identifier (possibly with leading/trailing whitespace)
|
||||
// A key-value has "identifier: something" or "'key': something"
|
||||
return cleaned.includes(':');
|
||||
});
|
||||
|
||||
if (hasKeyValue) {
|
||||
// Cannot convert to named exports - has computed values
|
||||
continue;
|
||||
}
|
||||
|
||||
// Convert export default { ... } → export { ... }
|
||||
const newContent = content.replace(/^export default (\{[\s\S]*?\});/m, 'export $1;');
|
||||
if (newContent !== content) {
|
||||
if (!DRY_RUN) fs.writeFileSync(file, newContent);
|
||||
convertedFiles.add(file);
|
||||
console.log(`✓ Converted exports: ${path.relative(ROOT, file)}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\nConverted ${convertedFiles.size} files to named exports.`);
|
||||
|
||||
// Phase 2: Update consumers
|
||||
// For each converted file, find all files that import it with `import X from './path'`
|
||||
// and change to `import * as X from './path'`
|
||||
let importChanges = 0;
|
||||
|
||||
for (const file of srcFiles) {
|
||||
let content = fs.readFileSync(file, 'utf-8');
|
||||
let changed = false;
|
||||
|
||||
// Match: import identifier from './relative/path.js';
|
||||
// or: import identifier from '../relative/path.js';
|
||||
const importRegex = /^(import\s+)(\w+)(\s+from\s+')(\.[^']+\.js)(';)$/gm;
|
||||
const newContent = content.replace(importRegex, (match, pre, name, mid, importPath, post) => {
|
||||
// Resolve the import path to absolute
|
||||
const dir = path.dirname(file);
|
||||
const resolved = path.resolve(dir, importPath);
|
||||
|
||||
if (convertedFiles.has(resolved)) {
|
||||
changed = true;
|
||||
importChanges++;
|
||||
return `${pre}* as ${name}${mid}${importPath}${post}`;
|
||||
}
|
||||
return match;
|
||||
});
|
||||
|
||||
if (changed && !DRY_RUN) {
|
||||
fs.writeFileSync(file, newContent);
|
||||
console.log(`✓ Updated imports: ${path.relative(ROOT, file)}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\nUpdated ${importChanges} import statements.`);
|
||||
console.log('Done.');
|
||||
@@ -0,0 +1,66 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// For a set of target files, find imports that use `import X from './module.js'`
|
||||
// where the module uses `export { ... }` (named exports, no default).
|
||||
// Convert those to `import * as X from './module.js'`.
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
const ROOT = path.resolve(import.meta.dirname, '..');
|
||||
const targetFiles = process.argv.slice(2).map(f => path.resolve(f));
|
||||
|
||||
if (targetFiles.length === 0) {
|
||||
console.log('Usage: node scripts/fix-imports-for-named.mjs file1.js file2.js ...');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Build a set of files that use named exports (export { ... }) without a default export
|
||||
function hasOnlyNamedExports(filePath) {
|
||||
if (!fs.existsSync(filePath)) return false;
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
const hasNamedExport = /^export \{/m.test(content);
|
||||
const hasDefaultExport = /^export default /m.test(content);
|
||||
return hasNamedExport && !hasDefaultExport;
|
||||
}
|
||||
|
||||
for (const filePath of targetFiles) {
|
||||
let content = fs.readFileSync(filePath, 'utf8');
|
||||
const relPath = path.relative(ROOT, filePath);
|
||||
const dir = path.dirname(filePath);
|
||||
let changes = 0;
|
||||
|
||||
// Find all default imports: import NAME from './something.js'
|
||||
const importRe = /^import (\w+) from '([^']+)';$/gm;
|
||||
let match;
|
||||
const replacements = [];
|
||||
|
||||
while ((match = importRe.exec(content)) !== null) {
|
||||
const [fullMatch, name, specifier] = match;
|
||||
|
||||
// Only handle local/relative imports
|
||||
if (!specifier.startsWith('.')) continue;
|
||||
|
||||
// Resolve the full path
|
||||
const resolved = path.resolve(dir, specifier);
|
||||
|
||||
if (hasOnlyNamedExports(resolved)) {
|
||||
replacements.push({
|
||||
from: fullMatch,
|
||||
to: `import * as ${name} from '${specifier}';`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
for (const r of replacements) {
|
||||
content = content.replace(r.from, r.to);
|
||||
changes++;
|
||||
}
|
||||
|
||||
if (changes > 0) {
|
||||
fs.writeFileSync(filePath, content);
|
||||
console.log(`${relPath}: fixed ${changes} imports`);
|
||||
} else {
|
||||
console.log(`${relPath}: no changes needed`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// Phase 3: Convert remaining export default { ... } (with computed properties)
|
||||
// to named exports by extracting computed values as const declarations.
|
||||
// Also updates consumers from `import X from` → `import * as X from`.
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
const ROOT = process.cwd();
|
||||
const DRY_RUN = process.argv.includes('--dry-run');
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const srcFiles = walk(path.join(ROOT, 'src'));
|
||||
srcFiles.push(path.join(ROOT, 'syslog.js'));
|
||||
|
||||
const convertedFiles = new Set();
|
||||
|
||||
for (const file of srcFiles) {
|
||||
let content = fs.readFileSync(file, 'utf-8');
|
||||
|
||||
// Match export default { ... };
|
||||
const match = content.match(/^(export default )(\{[\s\S]*?\});/m);
|
||||
if (!match) continue;
|
||||
|
||||
const fullMatch = match[0];
|
||||
const objContent = match[2];
|
||||
const inner = objContent.slice(1, -1); // strip { }
|
||||
|
||||
// Parse entries
|
||||
const lines = inner.split('\n');
|
||||
const extractedConsts = [];
|
||||
const exportNames = [];
|
||||
|
||||
for (const rawLine of lines) {
|
||||
const line = rawLine.trim();
|
||||
if (!line || line.startsWith('//') || line.startsWith('/*') || line.startsWith('*')) continue;
|
||||
|
||||
// Remove trailing comma
|
||||
const cleaned = line.replace(/,\s*$/, '').trim();
|
||||
if (!cleaned) continue;
|
||||
|
||||
// Check if it's a key-value pair (has : that's not in a string or ternary)
|
||||
// Simple heuristic: if the line matches "identifier: expression"
|
||||
const kvMatch = cleaned.match(/^(\w+)\s*:\s*(.+)$/);
|
||||
if (kvMatch) {
|
||||
const [, key, value] = kvMatch;
|
||||
// Check if this is truly a key-value or just a shorthand with same name
|
||||
// If key === value, it's shorthand (e.g. "x: x" is rare but possible)
|
||||
if (key !== value) {
|
||||
extractedConsts.push(`const ${key} = ${value};`);
|
||||
exportNames.push(key);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Shorthand property
|
||||
exportNames.push(cleaned);
|
||||
}
|
||||
|
||||
// Build new export block
|
||||
const newExport = `export {\n ${exportNames.join(',\n ')},\n};`;
|
||||
|
||||
// Build replacement: extracted consts + newline + export block
|
||||
let replacement;
|
||||
if (extractedConsts.length > 0) {
|
||||
replacement = extractedConsts.join('\n') + '\n\n' + newExport;
|
||||
} else {
|
||||
replacement = newExport;
|
||||
}
|
||||
|
||||
const newContent = content.replace(fullMatch, replacement);
|
||||
if (newContent !== content) {
|
||||
if (!DRY_RUN) fs.writeFileSync(file, newContent);
|
||||
convertedFiles.add(file);
|
||||
console.log(`✓ Converted exports: ${path.relative(ROOT, file)}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\nConverted ${convertedFiles.size} files.`);
|
||||
|
||||
// Phase 2: Update consumers
|
||||
let importChanges = 0;
|
||||
|
||||
for (const file of srcFiles) {
|
||||
let content = fs.readFileSync(file, 'utf-8');
|
||||
let changed = false;
|
||||
|
||||
const importRegex = /^(import\s+)(\w+)(\s+from\s+')(\.[^']+\.js)(';)$/gm;
|
||||
const newContent = content.replace(importRegex, (match, pre, name, mid, importPath, post) => {
|
||||
const dir = path.dirname(file);
|
||||
const resolved = path.resolve(dir, importPath);
|
||||
|
||||
if (convertedFiles.has(resolved)) {
|
||||
changed = true;
|
||||
importChanges++;
|
||||
return `${pre}* as ${name}${mid}${importPath}${post}`;
|
||||
}
|
||||
return match;
|
||||
});
|
||||
|
||||
if (changed && !DRY_RUN) {
|
||||
fs.writeFileSync(file, newContent);
|
||||
console.log(`✓ Updated imports: ${path.relative(ROOT, file)}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`\nUpdated ${importChanges} import statements.`);
|
||||
|
||||
// Verify no more export default { ... } remain
|
||||
let remaining = 0;
|
||||
for (const file of srcFiles) {
|
||||
const content = fs.readFileSync(file, 'utf-8');
|
||||
if (/^export default \{/m.test(content)) {
|
||||
console.log(`⚠ Still has export default: ${path.relative(ROOT, file)}`);
|
||||
remaining++;
|
||||
}
|
||||
}
|
||||
if (remaining > 0) console.log(`\n${remaining} files still need manual attention.`);
|
||||
else console.log('\nAll export default { ... } converted to named exports.');
|
||||
@@ -0,0 +1,60 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// Revert the broken extraction from fix-remaining-exports.mjs
|
||||
// and instead use a proper approach:
|
||||
// 1. Keep export default { ... } for files with computed properties
|
||||
// 2. Change all `import X from './local.js'` for those files to
|
||||
// `import * as _X from './local.js'; const X = _X.default;`
|
||||
// Actually that's ugly. Better approach:
|
||||
// Just lazify the top-level constant evaluation in services.js
|
||||
// OR use a re-export pattern.
|
||||
//
|
||||
// Actually, the simplest approach: restore the 34 files from git,
|
||||
// then re-run the first two conversion scripts on them,
|
||||
// then for files with complex exports, add a thin named-export wrapper.
|
||||
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { execSync } from 'node:child_process';
|
||||
|
||||
const ROOT = process.cwd();
|
||||
|
||||
// Find files that still have export default but with broken syntax
|
||||
const files = execSync('git diff --name-only', { cwd: ROOT, encoding: 'utf-8' })
|
||||
.trim().split('\n')
|
||||
.filter(f => f.startsWith('src/') || f === 'syslog.js');
|
||||
|
||||
// Check which files have syntax errors
|
||||
const brokenFiles = [];
|
||||
for (const file of files) {
|
||||
try {
|
||||
execSync(`node --check ${file}`, { cwd: ROOT, stdio: 'pipe' });
|
||||
} catch {
|
||||
brokenFiles.push(file);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Found ${brokenFiles.length} files with syntax errors:`);
|
||||
brokenFiles.forEach(f => console.log(` ${f}`));
|
||||
|
||||
// Restore these files from git
|
||||
for (const file of brokenFiles) {
|
||||
execSync(`git checkout -- ${file}`, { cwd: ROOT });
|
||||
console.log(`Restored: ${file}`);
|
||||
}
|
||||
|
||||
// Now re-run the initial CJS→ESM conversion on just these files
|
||||
console.log('\nRe-running CJS→ESM conversion on restored files...');
|
||||
const fileArgs = brokenFiles.join(' ');
|
||||
execSync(`node scripts/cjs-to-esm.mjs ${fileArgs}`, { cwd: ROOT, stdio: 'inherit' });
|
||||
|
||||
// Now re-run the first export fix (shorthand-only) on these files
|
||||
// But we need a targeted approach: for each file, check if its exports
|
||||
// are shorthand-only, and if so convert to named exports.
|
||||
// Files with computed properties will keep export default.
|
||||
|
||||
console.log('\nRe-running export fixes...');
|
||||
execSync('node scripts/fix-exports.mjs', { cwd: ROOT, stdio: 'inherit' });
|
||||
|
||||
console.log('\nDone. Files with complex exports will keep export default.');
|
||||
console.log('Circular dependency issues will be fixed by lazifying top-level access.');
|
||||
Reference in New Issue
Block a user