summaryrefslogtreecommitdiffstats
path: root/tools/esmify/import-to-import_esmodule.js
diff options
context:
space:
mode:
Diffstat (limited to 'tools/esmify/import-to-import_esmodule.js')
-rw-r--r--tools/esmify/import-to-import_esmodule.js472
1 files changed, 472 insertions, 0 deletions
diff --git a/tools/esmify/import-to-import_esmodule.js b/tools/esmify/import-to-import_esmodule.js
new file mode 100644
index 0000000000..d8e0aee5bb
--- /dev/null
+++ b/tools/esmify/import-to-import_esmodule.js
@@ -0,0 +1,472 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// jscodeshift rule to replace import calls for JSM with import calls for ESM
+// or static import for ESM.
+
+/* eslint-env node */
+
+const _path = require("path");
+const { isESMified } = require(_path.resolve(__dirname, "./is-esmified.js"));
+const {
+ jsmExtPattern,
+ esmifyExtension,
+ isIdentifier,
+ isString,
+ warnForPath,
+ getPrevStatement,
+ getNextStatement,
+ isMemberExpressionWithIdentifiers,
+ rewriteMemberExpressionWithIdentifiers,
+ createMemberExpressionWithIdentifiers,
+} = require(_path.resolve(__dirname, "./utils.js"));
+const {
+ isImportESModuleCall,
+ replaceImportESModuleCall,
+ tryReplacingWithStaticImport,
+} = require(_path.resolve(__dirname, "./static-import.js"));
+
+module.exports = function (fileInfo, api) {
+ const { jscodeshift } = api;
+ const root = jscodeshift(fileInfo.source);
+ doTranslate(fileInfo.path, jscodeshift, root);
+ return root.toSource({ lineTerminator: "\n" });
+};
+
+module.exports.doTranslate = doTranslate;
+
+function isESMifiedAndTarget(resourceURI) {
+ const files = [];
+ if (!isESMified(resourceURI, files)) {
+ return false;
+ }
+
+ if ("ESMIFY_TARGET_PREFIX" in process.env) {
+ const targetPrefix = process.env.ESMIFY_TARGET_PREFIX;
+ for (const esm of files) {
+ if (esm.startsWith(targetPrefix)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ return true;
+}
+
+const importCalls = [
+ {
+ from: ["Cu", "import"],
+ to: ["ChromeUtils", "importESModule"],
+ },
+ {
+ from: ["ChromeUtils", "import"],
+ to: ["ChromeUtils", "importESModule"],
+ },
+ {
+ from: ["SpecialPowers", "ChromeUtils", "import"],
+ to: ["SpecialPowers", "ChromeUtils", "importESModule"],
+ },
+];
+
+const singleLazyGetterCalls = [
+ {
+ from: ["ChromeUtils", "defineModuleGetter"],
+ to: ["ChromeUtils", "defineESModuleGetters"],
+ },
+ {
+ from: ["SpecialPowers", "ChromeUtils", "defineModuleGetter"],
+ to: ["SpecialPowers", "ChromeUtils", "defineESModuleGetters"],
+ },
+];
+
+const multiLazyGettersCalls = [
+ {
+ from: ["XPCOMUtils", "defineLazyModuleGetters"],
+ to: ["ChromeUtils", "defineESModuleGetters"],
+ },
+];
+
+function isMemberExpressionMatchingPatterns(node, patterns) {
+ for (const item of patterns) {
+ if (isMemberExpressionWithIdentifiers(node, item.from)) {
+ return item;
+ }
+ }
+
+ return null;
+}
+
+function replaceImportCall(inputFile, jscodeshift, path, rewriteItem) {
+ if (path.node.arguments.length !== 1) {
+ warnForPath(inputFile, path, `import call should have only one argument`);
+ return;
+ }
+
+ const resourceURINode = path.node.arguments[0];
+ if (!isString(resourceURINode)) {
+ warnForPath(inputFile, path, `resource URI should be a string`);
+ return;
+ }
+
+ const resourceURI = resourceURINode.value;
+ if (!resourceURI.match(jsmExtPattern)) {
+ warnForPath(inputFile, path, `Non-jsm: ${resourceURI}`);
+ return;
+ }
+
+ if (!isESMifiedAndTarget(resourceURI)) {
+ return;
+ }
+
+ if (
+ !tryReplacingWithStaticImport(
+ jscodeshift,
+ inputFile,
+ path,
+ resourceURINode,
+ false
+ )
+ ) {
+ rewriteMemberExpressionWithIdentifiers(path.node.callee, rewriteItem.to);
+ resourceURINode.value = esmifyExtension(resourceURI);
+ }
+}
+
+// Find `ChromeUtils.defineESModuleGetters` or variant statement specified by
+// expectedIDs, adjacent to `path` which uses the same target object.
+function findDefineESModuleGettersStmt(path, expectedIDs) {
+ // `path` must be top-level.
+ if (path.parent.node.type !== "ExpressionStatement") {
+ return null;
+ }
+
+ if (path.parent.parent.node.type !== "Program") {
+ return null;
+ }
+
+ // Get previous or next statement with ChromeUtils.defineESModuleGetters.
+ let callStmt;
+ const prev = getPrevStatement(path.parent);
+ if (
+ prev &&
+ prev.type === "ExpressionStatement" &&
+ prev.expression.type === "CallExpression" &&
+ isMemberExpressionWithIdentifiers(prev.expression.callee, expectedIDs)
+ ) {
+ callStmt = prev;
+ } else {
+ const next = getNextStatement(path.parent);
+ if (
+ next &&
+ next.type === "ExpressionStatement" &&
+ next.expression.type === "CallExpression" &&
+ isMemberExpressionWithIdentifiers(next.expression.callee, expectedIDs)
+ ) {
+ callStmt = next;
+ } else {
+ return null;
+ }
+ }
+
+ const call = callStmt.expression;
+
+ if (call.arguments.length !== 2) {
+ return null;
+ }
+
+ const modulesNode = call.arguments[1];
+ if (modulesNode.type !== "ObjectExpression") {
+ return null;
+ }
+
+ // Check if the target object is same.
+ if (
+ path.node.arguments[0].type === "ThisExpression" &&
+ call.arguments[0].type === "ThisExpression"
+ ) {
+ return callStmt;
+ }
+
+ if (
+ path.node.arguments[0].type === "Identifier" &&
+ call.arguments[0].type === "Identifier" &&
+ path.node.arguments[0].name === call.arguments[0].name
+ ) {
+ return callStmt;
+ }
+
+ return null;
+}
+
+function getPropKeyString(prop) {
+ if (prop.key.type === "Identifier") {
+ return prop.key.name;
+ }
+
+ if (prop.key.type === "Literal") {
+ return prop.key.value.toString();
+ }
+
+ return "";
+}
+
+function sortProps(obj) {
+ obj.properties.sort((a, b) => {
+ return getPropKeyString(a) < getPropKeyString(b) ? -1 : 1;
+ });
+}
+
+// Move comments above `nodeFrom` before `nodeTo`.
+function moveComments(nodeTo, nodeFrom) {
+ if (!nodeFrom.comments) {
+ return;
+ }
+ if (nodeTo.comments) {
+ nodeTo.comments = [...nodeTo.comments, ...nodeFrom.comments];
+ } else {
+ nodeTo.comments = nodeFrom.comments;
+ }
+ nodeFrom.comments = [];
+}
+
+function replaceLazyGetterCall(inputFile, jscodeshift, path, rewriteItem) {
+ if (path.node.arguments.length !== 3) {
+ warnForPath(inputFile, path, `lazy getter call should have 3 arguments`);
+ return;
+ }
+
+ const nameNode = path.node.arguments[1];
+ if (!isString(nameNode)) {
+ warnForPath(inputFile, path, `name should be a string`);
+ return;
+ }
+
+ const resourceURINode = path.node.arguments[2];
+ if (!isString(resourceURINode)) {
+ warnForPath(inputFile, path, `resource URI should be a string`);
+ return;
+ }
+
+ const resourceURI = resourceURINode.value;
+ if (!resourceURI.match(jsmExtPattern)) {
+ warnForPath(inputFile, path, `Non-js/jsm: ${resourceURI}`);
+ return;
+ }
+
+ if (!isESMifiedAndTarget(resourceURI)) {
+ return;
+ }
+
+ resourceURINode.value = esmifyExtension(resourceURI);
+ const prop = jscodeshift.property(
+ "init",
+ jscodeshift.identifier(nameNode.value),
+ resourceURINode
+ );
+
+ const callStmt = findDefineESModuleGettersStmt(path, rewriteItem.to);
+ if (callStmt) {
+ // Move a property to existing ChromeUtils.defineESModuleGetters call.
+
+ moveComments(callStmt, path.parent.node);
+ path.parent.prune();
+
+ callStmt.expression.arguments[1].properties.push(prop);
+ sortProps(callStmt.expression.arguments[1]);
+ } else {
+ // Convert this call into ChromeUtils.defineESModuleGetters.
+
+ rewriteMemberExpressionWithIdentifiers(path.node.callee, rewriteItem.to);
+ path.node.arguments = [
+ path.node.arguments[0],
+ jscodeshift.objectExpression([prop]),
+ ];
+ }
+}
+
+function replaceLazyGettersCall(inputFile, jscodeshift, path, rewriteItem) {
+ if (path.node.arguments.length !== 2) {
+ warnForPath(inputFile, path, `lazy getters call should have 2 arguments`);
+ return;
+ }
+
+ const modulesNode = path.node.arguments[1];
+ if (modulesNode.type !== "ObjectExpression") {
+ warnForPath(inputFile, path, `modules parameter should be an object`);
+ return;
+ }
+
+ const esmProps = [];
+ const jsmProps = [];
+
+ for (const prop of modulesNode.properties) {
+ const resourceURINode = prop.value;
+ if (!isString(resourceURINode)) {
+ warnForPath(inputFile, path, `resource URI should be a string`);
+ jsmProps.push(prop);
+ continue;
+ }
+
+ const resourceURI = resourceURINode.value;
+ if (!resourceURI.match(jsmExtPattern)) {
+ warnForPath(inputFile, path, `Non-js/jsm: ${resourceURI}`);
+ jsmProps.push(prop);
+ continue;
+ }
+
+ if (!isESMifiedAndTarget(resourceURI)) {
+ jsmProps.push(prop);
+ continue;
+ }
+
+ esmProps.push(prop);
+ }
+
+ if (esmProps.length === 0) {
+ return;
+ }
+
+ let callStmt = findDefineESModuleGettersStmt(path, rewriteItem.to);
+ if (jsmProps.length === 0) {
+ if (callStmt) {
+ // Move all properties to existing ChromeUtils.defineESModuleGetters call.
+
+ moveComments(callStmt, path.parent.node);
+ path.parent.prune();
+
+ for (const prop of esmProps) {
+ const resourceURINode = prop.value;
+ resourceURINode.value = esmifyExtension(resourceURINode.value);
+ callStmt.expression.arguments[1].properties.push(prop);
+ }
+ sortProps(callStmt.expression.arguments[1]);
+ } else {
+ // Convert this call into ChromeUtils.defineESModuleGetters.
+
+ rewriteMemberExpressionWithIdentifiers(path.node.callee, rewriteItem.to);
+ for (const prop of esmProps) {
+ const resourceURINode = prop.value;
+ resourceURINode.value = esmifyExtension(resourceURINode.value);
+ }
+ }
+ } else {
+ // Move some properties to ChromeUtils.defineESModuleGetters.
+
+ if (path.parent.node.type !== "ExpressionStatement") {
+ warnForPath(inputFile, path, `lazy getters call in unexpected context`);
+ return;
+ }
+
+ if (!callStmt) {
+ callStmt = jscodeshift.expressionStatement(
+ jscodeshift.callExpression(
+ createMemberExpressionWithIdentifiers(jscodeshift, rewriteItem.to),
+ [path.node.arguments[0], jscodeshift.objectExpression([])]
+ )
+ );
+ path.parent.insertBefore(callStmt);
+ }
+
+ moveComments(callStmt, path.parent.node);
+
+ for (const prop of esmProps) {
+ const resourceURINode = prop.value;
+ resourceURINode.value = esmifyExtension(resourceURINode.value);
+ callStmt.expression.arguments[1].properties.push(prop);
+ }
+ sortProps(callStmt.expression.arguments[1]);
+
+ path.node.arguments[1].properties = jsmProps;
+ }
+}
+
+function getProp(obj, key) {
+ if (obj.type !== "ObjectExpression") {
+ return null;
+ }
+
+ for (const prop of obj.properties) {
+ if (prop.computed) {
+ continue;
+ }
+
+ if (!prop.key) {
+ continue;
+ }
+
+ if (isIdentifier(prop.key, key)) {
+ return prop;
+ }
+ }
+
+ return null;
+}
+
+function tryReplaceActorDefinition(inputFile, path, name) {
+ const obj = path.node;
+
+ const prop = getProp(obj, name);
+ if (!prop) {
+ return;
+ }
+
+ const moduleURIProp = getProp(prop.value, "moduleURI");
+ if (!moduleURIProp) {
+ return;
+ }
+
+ if (!isString(moduleURIProp.value)) {
+ warnForPath(inputFile, path, `${name} moduleURI should be a string`);
+ return;
+ }
+
+ const moduleURI = moduleURIProp.value.value;
+ if (!moduleURI.match(jsmExtPattern)) {
+ warnForPath(inputFile, path, `${name} Non-js/jsm: ${moduleURI}`);
+ return;
+ }
+
+ if (!isESMifiedAndTarget(moduleURI)) {
+ return;
+ }
+
+ moduleURIProp.key.name = "esModuleURI";
+ moduleURIProp.value.value = esmifyExtension(moduleURI);
+}
+
+function doTranslate(inputFile, jscodeshift, root) {
+ root.find(jscodeshift.CallExpression).forEach(path => {
+ if (isImportESModuleCall(path.node)) {
+ replaceImportESModuleCall(inputFile, jscodeshift, path, false);
+ return;
+ }
+
+ const callee = path.node.callee;
+
+ let item;
+ item = isMemberExpressionMatchingPatterns(callee, importCalls);
+ if (item) {
+ replaceImportCall(inputFile, jscodeshift, path, item);
+ return;
+ }
+
+ item = isMemberExpressionMatchingPatterns(callee, singleLazyGetterCalls);
+ if (item) {
+ replaceLazyGetterCall(inputFile, jscodeshift, path, item);
+ return;
+ }
+
+ item = isMemberExpressionMatchingPatterns(callee, multiLazyGettersCalls);
+ if (item) {
+ replaceLazyGettersCall(inputFile, jscodeshift, path, item);
+ }
+ });
+
+ root.find(jscodeshift.ObjectExpression).forEach(path => {
+ tryReplaceActorDefinition(inputFile, path, "parent");
+ tryReplaceActorDefinition(inputFile, path, "child");
+ });
+}