Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
154 changes: 154 additions & 0 deletions scripts/migration/js2ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
#!/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<string, string>}
*/
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<string>} provides An array of strings with
* the names of the objects this file provides.
* @param {!Array<string>} _requires An array of strings with
* the names of the objects this file requires (unused).
* @param {boolean|!Object<string>=} 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;

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;
}
const relativePath =
path.relative(path.dirname(path.resolve(filename)), importPath);
if (name) {
return `import${type} * as ${name} from '${relativePath}';`;
} else if (names) {
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}.`);
}