#!/usr/bin/env node const fs = require('fs'); const path = require('path'); const filenames = process.argv.slice(2); // Trim off node and script name. ////////////////////////////////////////////////////////////////////// // Load deps files via require (since they're executalbe .js files). ////////////////////////////////////////////////////////////////////// /** * Dictionary mapping goog.module ID to absolute pathname of the file * containing the goog.declareModuleId for that ID. * @type {!Object} */ const modulePaths = {}; /** Absolute path of repository root. */ const repoPath = path.resolve(__dirname, '..', '..'); /** * Absolute path of directory containing base.js (the version used as * input to tsc, not the one output by it). * @type {string} */ const closurePath = path.resolve(repoPath, 'closure', 'goog'); globalThis.goog = {}; /** * Stub version of addDependency that store mappings in modulePaths. * @param {string} relPath The path to the js file. * @param {!Array} provides An array of strings with * the names of the objects this file provides. * @param {!Array} _requires An array of strings with * the names of the objects this file requires (unused). * @param {boolean|!Object=} opt_loadFlags Parameters indicating * how the file must be loaded. The boolean 'true' is equivalent * to {'module': 'goog'} for backwards-compatibility. Valid properties * and values include {'module': 'goog'} and {'lang': 'es6'}. */ goog.addDependency = function(relPath, provides, _requires, opt_loadFlags) { // Ignore any non-ESM files, as they can't be imported. if (opt_loadFlags?.module !== 'es6') return; // There should be only one "provide" from an ESM, but... for (const moduleId of provides) { // Store absolute path to source file (i.e., treating relPath // relative to closure/goog/, not build/src/closure/goog/). modulePaths[moduleId] = path.resolve(closurePath, relPath); } }; // Load deps files relative to this script's location. require(path.resolve(__dirname, '../../build/deps.js')); require(path.resolve(__dirname, '../../build/deps.mocha.js')); ////////////////////////////////////////////////////////////////////// // Process files mentioned on the command line. ////////////////////////////////////////////////////////////////////// /** RegExp matching goog.require statements. */ const requireRE = /(?:const\s+(?:([$\w]+)|(\{[^}]*\}))\s+=\s+)?goog.require(Type)?\('([^']+)'\);/mg; /** RegExp matching key: value pairs in destructuring assignments. */ const keyValueRE = /([$\w]+)\s*:\s*([$\w]+)\s*(?=,|})/g; 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} to TypeScript...`); // Remove "use strict". contents = contents.replace(/^\s*["']use strict["']\s*; *\n/m, ''); // Migrate from goog.module to goog.declareModuleId. const closurePathRelative = path.relative(path.dirname(path.resolve(filename)), closurePath); contents = contents.replace( /^goog.module\('([$\w.]+)'\);$/m, `import * as goog from '${closurePathRelative}/goog.js';\n` + `goog.declareModuleId('$1');`); // Migrate from goog.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. type, // If truthy, it is a requireType not require. moduleId, // goog.module ID that was goog.require()d. ) { const importPath = modulePaths[moduleId]; type = type ? ' type' : ''; if (!importPath) { console.warn(`Unable to migrate goog.require('${ moduleId}') as no ES module path known.`); return orig; } let relativePath = path.relative(path.dirname(path.resolve(filename)), importPath); if (relativePath[0] !== '.') relativePath = './' + relativePath; if (name) { return `import${type} * as ${name} from '${relativePath}';`; } else if (names) { names = names.replace(keyValueRE, '$1 as $2'); return `import${type} ${names} from '${relativePath}';`; } else { // Side-effect only require. return `import${type} '${relativePath}';`; } }); // Find and update or remove old-style export assignemnts. /** @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 transalted 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*)((?:const|let|var|function|class)\\s+${declName})\\b`, 'gm'); if (contents.match(declRE)) { easyExports.push({exportName, declRE}); return ''; // Delete existing export assignment. } else { return `export ${exportName};\n`; // Safe fallback. } }); // 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(/.js$/, '.ts'); fs.writeFileSync(newFilename, contents); console.log(`Wrote ${newFilename}.`); }