Files
blockly/scripts/migration/js2ts
Christopher Allen 4d2201a427 chore(generators): Migrate generators to ES Modules (#7103)
* feat(j2ts): Add support for migrating renaming imports

  Convert
      const {foo: bar} = require(/*...*/);
  into
      import {foo as bar} from /*...*/;
              ^^^^^^^^^^

  Also fix a bug that caused relative paths to ESM in the same
  directory to be missing a leading "./".

* fix(build): Fix trivial error exports for generators

  The UMD wrapper was inadvertently exporting the contents of (e.g.)
  the Blockly.JavaScript closure module rather than the intended
  export of Blockly.JavaScript.all module - which went unnoticed
  because the latter just reexported the former - but we are
  about to convert the former to ESM.

* chore(generators): Migrate language generators to ESM

  Migrate the main language generators in generators/*.js to ESM.

  This was done by running js2ts on the files, renaming them back
  to .js, and commenting out "import type" statements, which are
  legal TS but not needed in JS (at least if you are not actually
  letting Closure Compiler do type checking, which we are not.)

* chore(generators): Migrate block generators to ESM

  Migrate generators/*/*.js (except all.js) to ESM.

  This was done by running js2ts on the files, renaming them back
  to .js, and removing now-spurious @suppress {extraRequire}
  directives.

* chores(generators): Migrate generator chunk entrypoints to ESM

  This was done by running js2ts on the files, renaming them back
  to .js, and manually fixing the export statements.

  An additional change to the chunk exports configuration in
  build_tasks.js was necessary in order for the UMD wrapper to
  find the new module object, which is given a different name
  than the old exports object.
2023-05-19 23:09:37 +01:00

160 lines
6.1 KiB
JavaScript
Executable File

#!/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;
/** 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}.`);
}