#!/usr/bin/env node const fs = require('fs'); const path = require('path'); const filenames = process.argv.slice(2); // Trim off node and script name. /** Absolute path of repository root. */ const repoPath = path.resolve(__dirname, '..', '..'); ////////////////////////////////////////////////////////////////////// // Process files mentioned on the command line. ////////////////////////////////////////////////////////////////////// /** RegExp matching require statements. */ const requireRE = /(?:const\s+(?:([$\w]+)|(\{[^}]*\}))\s*=\s*)?require\('([^']+)'\);/g; /** RegExp matching key: value pairs in destructuring assignments. */ const keyValueRE = /([$\w]+)\s*:\s*([$\w]+)\s*(?=,|})/g; /** Prefix for RegExp matching a top-level declaration. */ const declPrefix = '(?:const|let|var|(?:async\\s+)?function(?:\\s*\\*)?|class)'; for (const filename of filenames) { let contents = null; try { contents = String(fs.readFileSync(filename)); } catch (e) { console.error(`error while reading ${filename}: ${e.message}`); continue; } console.log(`Converting ${filename} from CJS to ESM...`); // Remove "use strict". contents = contents.replace(/^\s*["']use strict["']\s*; *\n/m, ''); // Migrate from require to import. contents = contents.replace( requireRE, function ( orig, // Whole statement to be replaced. name, // Name of named import of whole module (if applicable). names, // {}-enclosed list of destructured imports. moduleName, // Imported module name or path. ) { if (moduleName[0] === '.') { // Relative path. Could check and add '.mjs' suffix if desired. } if (name) { return `import * as ${name} from '${moduleName}';`; } else if (names) { names = names.replace(keyValueRE, '$1 as $2'); return `import ${names} from '${moduleName}';`; } else { // Side-effect only require. return `import '${moduleName}';`; } }, ); // Find and update or remove old-style single-export assignments // like: // // exports.bar = foo; // becomes export {foo as bar}; // exports.foo = foo; // remove the export and export at declaration // // instead, if possible. /** @type {!Array<{name: string, re: RegExp>}>} */ const easyExports = []; contents = contents.replace( /^\s*exports\.([$\w]+)\s*=\s*([$\w]+)\s*;\n/gm, function ( orig, // Whole statement to be replaced. exportName, // Name to export item as. declName, // Already-declared name for item being exported. ) { // Renamed exports have to be translated as-is. if (exportName !== declName) { return `export {${declName} as ${exportName}};\n`; } // OK, we're doing "export.foo = foo;". Can we update the // declaration? We can't actualy modify it yet as we're in // the middle of a search-and-replace on contents already, but // we can delete the old export and later update the // declaration into an export. const declRE = new RegExp( `^(\\s*)(${declPrefix}\\s+${declName})\\b`, 'gm', ); if (contents.match(declRE)) { easyExports.push({exportName, declRE}); return ''; // Delete existing export assignment. } else { return `export ${exportName};\n`; // Safe fallback. } }, ); // Find and update or remove old-style module.exports assignment // like: // // module.exports = {foo, bar: baz, quux}; // // which becomes export {baz as bar}, with foo and quux exported at // declaration instead, if possible. contents = contents.replace( /^module\.exports\s*=\s*\{([^\}]+)\};?(\n?)/m, function ( orig, // Whole statement to be replaced. items, // List of items to be exported. ) { items = items.replace( /( *)([$\w]+)\s*(?::\s*([$\w]+)\s*)?,?(\s*?\n?)/gm, function ( origItem, // Whole item being replaced. indent, // Optional leading whitespace. exportName, // Name to export item as. declName, // Already-declared name being exported, if different. newline, // Optional trailing whitespace. ) { if (!declName) declName = exportName; // Renamed exports have to be translated as-is. if (exportName !== declName) { return `${indent}${declName} as ${exportName},${newline}`; } // OK, this item has no rename. Can we update the // declaration? We can't actualy modify it yet as we're in // the middle of a search-and-replace on contents already, // but we can delete the item and later update the // declaration into an export. const declRE = new RegExp( `^(\\s*)(${declPrefix}\\s+${declName})\\b`, 'gm', ); if (contents.match(declRE)) { easyExports.push({exportName, declRE}); return ''; // Delete existing item. } else { return `${indent}${exportName},${newline}`; // Safe fallback. } }, ); if (/^\s*$/s.test(items)) { // No items left? return ''; // Delete entire module.export assignment. } else { return `export {${items}};\n`; } }, ); // Add 'export' to existing declarations where appropriate. for (const {exportName, declRE} of easyExports) { contents = contents.replace(declRE, '$1export $2'); } // Write converted file with new extension. const newFilename = filename.replace(/.c?js$/, '.mjs'); fs.writeFileSync(newFilename, contents); console.log(`Wrote ${newFilename}.`); }