mirror of
https://github.com/zotero/zotero.git
synced 2026-09-07 08:26:14 +00:00
940 lines
28 KiB
Python
Executable file
940 lines
28 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
|
|
# 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/.
|
|
|
|
import json
|
|
import logging
|
|
import os
|
|
import pathlib
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
import argparse
|
|
|
|
|
|
def path_sep_to_native(path_str):
|
|
"""Make separators in the path OS native."""
|
|
return os.sep.join(path_str.split("/"))
|
|
|
|
|
|
def path_sep_from_native(path):
|
|
"""Make separators in the path OS native."""
|
|
return "/".join(str(path).split(os.sep))
|
|
|
|
|
|
excluded_from_convert_prefix = list(
|
|
map(
|
|
path_sep_to_native,
|
|
[
|
|
# Testcases for actors.
|
|
"toolkit/actors/TestProcessActorChild.jsm",
|
|
"toolkit/actors/TestProcessActorParent.jsm",
|
|
"toolkit/actors/TestWindowChild.jsm",
|
|
"toolkit/actors/TestWindowParent.jsm",
|
|
"js/xpconnect/tests/unit/",
|
|
# Testcase for build system.
|
|
"python/mozbuild/mozbuild/test/",
|
|
],
|
|
)
|
|
)
|
|
|
|
|
|
def is_excluded_from_convert(path):
|
|
"""Returns true if the JSM file shouldn't be converted to ESM."""
|
|
path_str = str(path)
|
|
for prefix in excluded_from_convert_prefix:
|
|
if path_str.startswith(prefix):
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
excluded_from_imports_prefix = list(
|
|
map(
|
|
path_sep_to_native,
|
|
[
|
|
# Vendored or auto-generated files.
|
|
"browser/components/pocket/content/panels/js/vendor.bundle.js",
|
|
"devtools/client/debugger/dist/parser-worker.js",
|
|
"devtools/client/debugger/test/mochitest/examples/react/build/main.js",
|
|
"devtools/client/debugger/test/mochitest/examples/sourcemapped/polyfill-bundle.js",
|
|
"devtools/client/inspector/markup/test/shadowdom_open_debugger.min.js",
|
|
"devtools/client/shared/source-map-loader/test/browser/fixtures/bundle.js",
|
|
"layout/style/test/property_database.js",
|
|
"services/fxaccounts/FxAccountsPairingChannel.js",
|
|
"testing/talos/talos/tests/devtools/addon/content/pages/custom/debugger/static/js/main.js", # noqa E501
|
|
"testing/web-platform/",
|
|
# Unrelated testcases that has edge case syntax.
|
|
"browser/components/sessionstore/test/unit/data/",
|
|
"devtools/client/debugger/src/workers/parser/tests/fixtures/",
|
|
"devtools/client/debugger/test/mochitest/examples/sourcemapped/fixtures/",
|
|
"devtools/client/webconsole/test/browser/test-syntaxerror-worklet.js",
|
|
"devtools/server/tests/xpcshell/test_framebindings-03.js",
|
|
"devtools/server/tests/xpcshell/test_framebindings-04.js",
|
|
"devtools/shared/tests/xpcshell/test_eventemitter_basic.js",
|
|
"devtools/shared/tests/xpcshell/test_eventemitter_static.js",
|
|
"dom/base/crashtests/module-with-syntax-error.js",
|
|
"dom/base/test/file_bug687859-16.js",
|
|
"dom/base/test/file_bug687859-16.js",
|
|
"dom/base/test/file_js_cache_syntax_error.js",
|
|
"dom/base/test/jsmodules/module_badSyntax.js",
|
|
"dom/canvas/test/reftest/webgl-utils.js",
|
|
"dom/encoding/test/file_utf16_be_bom.js",
|
|
"dom/encoding/test/file_utf16_le_bom.js",
|
|
"dom/html/test/bug649134/file_bug649134-1.sjs",
|
|
"dom/html/test/bug649134/file_bug649134-2.sjs",
|
|
"dom/media/webrtc/tests/mochitests/identity/idp-bad.js",
|
|
"dom/serviceworkers/test/file_js_cache_syntax_error.js",
|
|
"dom/serviceworkers/test/parse_error_worker.js",
|
|
"dom/workers/test/importScripts_worker_imported3.js",
|
|
"dom/workers/test/invalid.js",
|
|
"dom/workers/test/threadErrors_worker1.js",
|
|
"dom/xhr/tests/browser_blobFromFile.js",
|
|
"image/test/browser/browser_image.js",
|
|
"js/xpconnect/tests/chrome/test_bug732665_meta.js",
|
|
"js/xpconnect/tests/mochitest/class_static_worker.js",
|
|
"js/xpconnect/tests/unit/bug451678_subscript.js",
|
|
"js/xpconnect/tests/unit/error_other.sys.mjs",
|
|
"js/xpconnect/tests/unit/es6module_parse_error.js",
|
|
"js/xpconnect/tests/unit/recursive_importA.jsm",
|
|
"js/xpconnect/tests/unit/recursive_importB.jsm",
|
|
"js/xpconnect/tests/unit/syntax_error.jsm",
|
|
"js/xpconnect/tests/unit/test_defineModuleGetter.js",
|
|
"js/xpconnect/tests/unit/test_import.js",
|
|
"js/xpconnect/tests/unit/test_import_shim.js",
|
|
"js/xpconnect/tests/unit/test_recursive_import.js",
|
|
"js/xpconnect/tests/unit/test_unload.js",
|
|
"modules/libpref/test/unit/data/testParser.js",
|
|
"python/mozbuild/mozbuild/test/",
|
|
"remote/shared/messagehandler/test/browser/resources/modules/root/invalid.sys.mjs",
|
|
"testing/talos/talos/startup_test/sessionrestore/profile-manywindows/sessionstore.js",
|
|
"testing/talos/talos/startup_test/sessionrestore/profile/sessionstore.js",
|
|
"toolkit/components/reader/Readerable.sys.mjs",
|
|
"toolkit/components/workerloader/tests/moduleF-syntax-error.js",
|
|
"tools/lint/test/",
|
|
"tools/update-packaging/test/",
|
|
# SpiderMonkey internals.
|
|
"js/examples/",
|
|
"js/src/",
|
|
# Files has macro.
|
|
"browser/app/profile/firefox.js",
|
|
"browser/branding/official/pref/firefox-branding.js",
|
|
"browser/components/enterprisepolicies/schemas/schema.sys.mjs",
|
|
"browser/locales/en-US/firefox-l10n.js",
|
|
"mobile/android/app/geckoview-prefs.js",
|
|
"mobile/android/app/mobile.js",
|
|
"mobile/android/locales/en-US/mobile-l10n.js",
|
|
"modules/libpref/greprefs.js",
|
|
"modules/libpref/init/all.js",
|
|
"testing/condprofile/condprof/tests/profile/user.js",
|
|
"testing/mozbase/mozprofile/tests/files/prefs_with_comments.js",
|
|
"toolkit/modules/AppConstants.sys.mjs",
|
|
"toolkit/mozapps/update/tests/data/xpcshellConstantsPP.js",
|
|
# Uniffi templates
|
|
"toolkit/components/uniffi-bindgen-gecko-js/src/templates/js/",
|
|
"node_modules/",
|
|
"resource/tinymce/",
|
|
"resource/react",
|
|
],
|
|
)
|
|
)
|
|
|
|
EXCLUSION_FILES = []
|
|
|
|
|
|
def load_exclusion_files():
|
|
for path in EXCLUSION_FILES:
|
|
with open(path, "r") as f:
|
|
for line in f:
|
|
p = path_sep_to_native(re.sub(r"\*$", "", line.strip()))
|
|
excluded_from_imports_prefix.append(p)
|
|
|
|
|
|
def is_excluded_from_imports(path):
|
|
"""Returns true if the JS file content shouldn't be handled by
|
|
jscodeshift.
|
|
|
|
This filter is necessary because jscodeshift cannot handle some
|
|
syntax edge cases and results in unexpected rewrite."""
|
|
path_str = str(path)
|
|
for prefix in excluded_from_imports_prefix:
|
|
if path_str.startswith(prefix):
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
# Wrapper for hg/git operations
|
|
class VCSUtils:
|
|
def run(self, cmd):
|
|
# Do not pass check=True because the pattern can match no file.
|
|
lines = subprocess.run(cmd, stdout=subprocess.PIPE).stdout.decode()
|
|
return filter(lambda x: x != "", lines.split("\n"))
|
|
|
|
|
|
class GitUtils(VCSUtils):
|
|
def is_available():
|
|
return pathlib.Path(".git").exists()
|
|
|
|
def rename(self, before, after):
|
|
cmd = ["git", "mv", before, after]
|
|
subprocess.run(cmd, check=True)
|
|
|
|
def find_jsms(self, path):
|
|
jsms = []
|
|
|
|
cmd = ["git", "ls-files", f"{path}/*.jsm"]
|
|
for line in self.run(cmd):
|
|
jsm = pathlib.Path(line)
|
|
if is_excluded_from_convert(jsm):
|
|
continue
|
|
jsms.append(jsm)
|
|
|
|
handled = {}
|
|
cmd = ["git", "grep", r"EXPORTED_SYMBOLS = \[", f"{path}/*.js"]
|
|
for line in self.run(cmd):
|
|
m = re.search("^([^:]+):", line)
|
|
if not m:
|
|
continue
|
|
filename = m.group(1)
|
|
if filename in handled:
|
|
continue
|
|
handled[filename] = True
|
|
jsm = pathlib.Path(filename)
|
|
if is_excluded_from_convert(jsm):
|
|
continue
|
|
jsms.append(jsm)
|
|
|
|
return jsms
|
|
|
|
def find_all_jss(self, path):
|
|
jss = []
|
|
|
|
cmd = [
|
|
"git",
|
|
"ls-files",
|
|
f"{path}/*.jsm",
|
|
f"{path}/*.js",
|
|
f"{path}/*.mjs",
|
|
f"{path}/*.sjs",
|
|
]
|
|
for line in self.run(cmd):
|
|
js = pathlib.Path(line)
|
|
if is_excluded_from_imports(js):
|
|
continue
|
|
jss.append(js)
|
|
|
|
return jss
|
|
|
|
|
|
def esmify(path=None, convert=False, imports=False, prefix=""):
|
|
class Summary:
|
|
def __init__(self):
|
|
self.convert_errors = []
|
|
self.import_errors = []
|
|
self.rename_errors = []
|
|
self.no_refs = []
|
|
|
|
class CommandContext:
|
|
def log(self, level, tag, _, message):
|
|
print(f"{tag}: {message}")
|
|
command_context = CommandContext()
|
|
|
|
"""
|
|
This command does the following 2 steps:
|
|
1. Convert the JSM file specified by `path` to ESM file, or the JSM files
|
|
inside the directory specified by `path` to ESM files, and also
|
|
fix references in build files and test definitions
|
|
2. Convert import calls inside file(s) specified by `path` for ESM-ified
|
|
files to use new APIs
|
|
|
|
Example 1:
|
|
# Convert all JSM files inside `chrome/content/zotero/xpcom` directory,
|
|
# and replace all references for ESM-ified files in the entire tree to use
|
|
# new APIs
|
|
|
|
$ python3 scripts/migrate-fx140/migrate.py esmify --convert chrome/content/zotero/xpcom
|
|
$ python3 scripts/migrate-fx140/migrate.py esmify --imports . --prefix=chrome/content/zotero/xpcom
|
|
|
|
Example 2:
|
|
# Convert all JSM files inside `chrome` directory, and replace all
|
|
# references for the JSM files inside `chrome` directory to use
|
|
# new APIs
|
|
|
|
$ python3 scripts/migrate-fx140/migrate.py esmify chrome
|
|
"""
|
|
|
|
def error(text):
|
|
command_context.log(logging.ERROR, "esmify", {}, f"[ERROR] {text}")
|
|
|
|
def warn(text):
|
|
command_context.log(logging.WARN, "esmify", {}, f"[WARN] {text}")
|
|
|
|
def info(text):
|
|
command_context.log(logging.INFO, "esmify", {}, f"[INFO] {text}")
|
|
|
|
# If no options is specified, perform both.
|
|
if not convert and not imports:
|
|
convert = True
|
|
imports = True
|
|
|
|
path = pathlib.Path(path[0])
|
|
|
|
if not verify_path(command_context, path):
|
|
return 1
|
|
|
|
if GitUtils.is_available():
|
|
vcs_utils = GitUtils()
|
|
else:
|
|
error(
|
|
"This script needs to be run inside a zotero-client Git clone."
|
|
)
|
|
return 1
|
|
|
|
load_exclusion_files()
|
|
|
|
is_single_file = path.is_file()
|
|
|
|
modified_files = []
|
|
summary = Summary()
|
|
|
|
if convert:
|
|
info("Searching files to convert to ESM...")
|
|
if is_single_file:
|
|
jsms = [path]
|
|
else:
|
|
jsms = vcs_utils.find_jsms(path)
|
|
|
|
info(f"Found {len(jsms)} file(s) to convert to ESM.")
|
|
|
|
info("Converting to ESM...")
|
|
jsms = esmify_convert_module(jsms, summary)
|
|
if jsms is None:
|
|
error("Failed to rewrite exports.")
|
|
return 1
|
|
|
|
info("Renaming...")
|
|
esms = rename_jsms(command_context, vcs_utils, jsms, summary)
|
|
|
|
modified_files += esms
|
|
|
|
if imports:
|
|
info("Searching files to rewrite imports...")
|
|
|
|
if is_single_file:
|
|
if convert:
|
|
# Already converted above
|
|
jss = esms
|
|
else:
|
|
jss = [path]
|
|
else:
|
|
jss = vcs_utils.find_all_jss(path)
|
|
|
|
info(f"Checking {len(jss)} JS file(s). Rewriting any matching imports...")
|
|
|
|
result = esmify_rewrite_imports(jss, prefix, summary)
|
|
if result is None:
|
|
return 1
|
|
|
|
info(f"Rewritten {len(result)} file(s).")
|
|
|
|
# Only modified files needs eslint fix
|
|
modified_files += result
|
|
|
|
modified_files = list(set(modified_files))
|
|
|
|
info(f"Applying eslint --fix for {len(modified_files)} file(s)...")
|
|
eslint_fix(command_context, modified_files)
|
|
|
|
def print_files(f, errors):
|
|
for [path, message] in errors:
|
|
f(f" * {path}")
|
|
if message:
|
|
f(f" {message}")
|
|
|
|
if len(summary.convert_errors):
|
|
error("========")
|
|
error("The following files were not converted into ESM due to errors:")
|
|
print_files(error, summary.convert_errors)
|
|
|
|
if len(summary.import_errors):
|
|
warn("========")
|
|
warn("The following files were not rewritten to import ESMs due to errors:")
|
|
warn(
|
|
"(NOTE: Errors related to 'private names' are mostly due to "
|
|
" preprocessor macros in the file):"
|
|
)
|
|
print_files(warn, summary.import_errors)
|
|
|
|
if len(summary.rename_errors):
|
|
error("========")
|
|
error("The following files were not renamed due to errors:")
|
|
print_files(error, summary.rename_errors)
|
|
|
|
if len(summary.no_refs):
|
|
warn("========")
|
|
warn("The following files were not found in any build files.")
|
|
warn("Please update references to these files manually:")
|
|
print_files(warn, summary.rename_errors)
|
|
|
|
return 0
|
|
|
|
|
|
def esmify_convert_module(jsms, summary):
|
|
"""Replace EXPORTED_SYMBOLS with export declarations, and replace
|
|
ChromeUtils.importESModule with static import as much as possible,
|
|
and return the list of successfully rewritten files."""
|
|
|
|
if len(jsms) == 0:
|
|
return []
|
|
|
|
env = os.environ.copy()
|
|
|
|
stdin = "\n".join(map(str, jsms)).encode()
|
|
|
|
ok_files, errors = run_jscodeshift(
|
|
"use-import-export-declarations.js",
|
|
env=env,
|
|
stdin=stdin,
|
|
)
|
|
|
|
if ok_files is None and errors is None:
|
|
return None
|
|
|
|
summary.convert_errors.extend(errors)
|
|
|
|
return ok_files
|
|
|
|
|
|
def esmify_rewrite_imports(jss, prefix, summary):
|
|
"""Replace import calls for JSM with import calls for ESM or static import
|
|
for ESM."""
|
|
|
|
if len(jss) == 0:
|
|
return []
|
|
|
|
env = os.environ.copy()
|
|
env["ESMIFY_TARGET_PREFIX"] = prefix
|
|
|
|
stdin = "\n".join(map(str, jss)).encode()
|
|
|
|
ok_files, errors = run_jscodeshift(
|
|
"import-to-import_esmodule.js",
|
|
env=env,
|
|
stdin=stdin,
|
|
)
|
|
|
|
if ok_files is None and errors is None:
|
|
return None
|
|
|
|
summary.import_errors.extend(errors)
|
|
|
|
return ok_files
|
|
|
|
|
|
def asyncify(path=None):
|
|
class Summary:
|
|
def __init__(self):
|
|
self.convert_errors = []
|
|
|
|
class CommandContext:
|
|
def log(self, level, tag, _, message):
|
|
print(f"{tag}: {message}")
|
|
command_context = CommandContext()
|
|
|
|
def error(text):
|
|
command_context.log(logging.ERROR, "asyncify", {}, f"[ERROR] {text}")
|
|
|
|
def warn(text):
|
|
command_context.log(logging.WARN, "asyncify", {}, f"[WARN] {text}")
|
|
|
|
def info(text):
|
|
command_context.log(logging.INFO, "asyncify", {}, f"[INFO] {text}")
|
|
|
|
path = pathlib.Path(path[0])
|
|
|
|
if not verify_path(command_context, path):
|
|
return 1
|
|
|
|
if GitUtils.is_available():
|
|
vcs_utils = GitUtils()
|
|
else:
|
|
error(
|
|
"This script needs to be run inside a zotero-client Git clone."
|
|
)
|
|
return 1
|
|
|
|
load_exclusion_files()
|
|
|
|
is_single_file = path.is_file()
|
|
|
|
summary = Summary()
|
|
|
|
info("Searching files to rewrite Bluebird methods...")
|
|
|
|
if is_single_file:
|
|
jss = [path]
|
|
else:
|
|
jss = vcs_utils.find_all_jss(path)
|
|
|
|
info(f"Checking {len(jss)} JS file(s). Rewriting Bluebird methods...")
|
|
|
|
modified_files = asyncify_rewrite_bluebird_methods(jss, summary)
|
|
if modified_files is None:
|
|
return 1
|
|
|
|
info(f"Rewritten {len(modified_files)} file(s).")
|
|
|
|
info(f"Applying eslint --fix for {len(modified_files)} file(s)...")
|
|
eslint_fix(command_context, modified_files)
|
|
|
|
def print_files(f, errors):
|
|
for [path, message] in errors:
|
|
f(f" * {path}")
|
|
if message:
|
|
f(f" {message}")
|
|
|
|
if len(summary.convert_errors):
|
|
error("========")
|
|
error("The following files were not migrated to use async due to errors:")
|
|
print_files(error, summary.convert_errors)
|
|
|
|
return 0
|
|
|
|
|
|
def asyncify_rewrite_bluebird_methods(jss, summary):
|
|
if len(jss) == 0:
|
|
return []
|
|
|
|
env = os.environ.copy()
|
|
stdin = "\n".join(map(str, jss)).encode()
|
|
|
|
ok_files, errors = run_jscodeshift(
|
|
"bluebird-to-async.js",
|
|
env=env,
|
|
stdin=stdin,
|
|
)
|
|
|
|
if ok_files is None and errors is None:
|
|
return None
|
|
|
|
summary.convert_errors.extend(errors)
|
|
|
|
return ok_files
|
|
|
|
|
|
def verify_path(command_context, path):
|
|
"""Check if the path passed to the command is valid relative path."""
|
|
|
|
def error(text):
|
|
command_context.log(logging.ERROR, "migrate", {}, f"[ERROR] {text}")
|
|
|
|
if not path.exists():
|
|
error(f"{path} does not exist.")
|
|
return False
|
|
|
|
if path.is_absolute():
|
|
error("Path must be a relative path from zotero-client checkout.")
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
def find_file(path, target):
|
|
"""Find `target` file in ancestor of path."""
|
|
target_path = path.parent / target
|
|
if not target_path.exists():
|
|
if path.parent == path:
|
|
return None
|
|
|
|
return find_file(path.parent, target)
|
|
|
|
return target_path
|
|
|
|
|
|
def try_rename_in(command_context, path, target, jsm_name, esm_name, jsm_path):
|
|
"""Replace the occurrences of `jsm_name` with `esm_name` in `target`
|
|
file."""
|
|
|
|
def info(text):
|
|
command_context.log(logging.INFO, "esmify", {}, f"[INFO] {text}")
|
|
|
|
if type(target) is str:
|
|
# Target is specified by filename, that may exist somewhere in
|
|
# the jsm's directory or ancestor directories.
|
|
target_path = find_file(path, target)
|
|
if not target_path:
|
|
return False
|
|
|
|
# JSM should be specified with relative path in the file.
|
|
#
|
|
# Single moz.build or jar.mn can contain multiple files with same name.
|
|
# Search for relative path.
|
|
jsm_relative_path = jsm_path.relative_to(target_path.parent)
|
|
jsm_path_str = path_sep_from_native(str(jsm_relative_path))
|
|
else:
|
|
# Target is specified by full path.
|
|
target_path = target
|
|
|
|
# JSM should be specified with full path in the file.
|
|
jsm_path_str = path_sep_from_native(str(jsm_path))
|
|
|
|
jsm_path_re = re.compile(r"\b" + jsm_path_str.replace(".", r"\.") + r"\b")
|
|
jsm_name_re = re.compile(r"\b" + jsm_name.replace(".", r"\.") + r"\b")
|
|
|
|
modified = False
|
|
content = ""
|
|
with open(target_path, "r") as f:
|
|
for line in f:
|
|
if jsm_path_re.search(line):
|
|
modified = True
|
|
line = jsm_name_re.sub(esm_name, line)
|
|
|
|
content += line
|
|
|
|
if modified:
|
|
info(f" {str(target_path)}")
|
|
info(f" {jsm_name} => {esm_name}")
|
|
with open(target_path, "w", newline="\n") as f:
|
|
f.write(content)
|
|
|
|
return True
|
|
|
|
|
|
def try_rename_uri_in(command_context, target, jsm_name, esm_name, jsm_uri, esm_uri):
|
|
"""Replace the occurrences of `jsm_uri` with `esm_uri` in `target` file."""
|
|
|
|
def info(text):
|
|
command_context.log(logging.INFO, "esmify", {}, f"[INFO] {text}")
|
|
|
|
modified = False
|
|
content = ""
|
|
with open(target, "r") as f:
|
|
for line in f:
|
|
if jsm_uri in line:
|
|
modified = True
|
|
line = line.replace(jsm_uri, esm_uri)
|
|
|
|
content += line
|
|
|
|
if modified:
|
|
info(f" {str(target)}")
|
|
info(f" {jsm_name} => {esm_name}")
|
|
with open(target, "w", newline="\n") as f:
|
|
f.write(content)
|
|
|
|
return True
|
|
|
|
|
|
def try_rename_components_conf(command_context, path, jsm_name, esm_name):
|
|
"""Replace the occurrences of `jsm_name` with `esm_name` in components.conf
|
|
file."""
|
|
|
|
def info(text):
|
|
command_context.log(logging.INFO, "esmify", {}, f"[INFO] {text}")
|
|
|
|
target_path = find_file(path, "components.conf")
|
|
if not target_path:
|
|
return False
|
|
|
|
# Unlike try_rename_in, components.conf contains the URL instead of
|
|
# relative path, and also there are no known files with same name.
|
|
# Simply replace the filename.
|
|
|
|
with open(target_path, "r") as f:
|
|
content = f.read()
|
|
|
|
prop_re = re.compile(
|
|
"[\"']jsm[\"']:(.*)" + r"\b" + jsm_name.replace(".", r"\.") + r"\b"
|
|
)
|
|
|
|
if not prop_re.search(content):
|
|
return False
|
|
|
|
info(f" {str(target_path)}")
|
|
info(f" {jsm_name} => {esm_name}")
|
|
|
|
content = prop_re.sub(r"'esModule':\1" + esm_name, content)
|
|
with open(target_path, "w", newline="\n") as f:
|
|
f.write(content)
|
|
|
|
return True
|
|
|
|
|
|
def esmify_name(name):
|
|
return re.sub(r"\.(jsm|js|jsm\.js)$", ".mjs", name)
|
|
|
|
|
|
def esmify_path(jsm_path):
|
|
jsm_name = jsm_path.name
|
|
esm_name = re.sub(r"\.(jsm|js|jsm\.js)$", ".mjs", jsm_name)
|
|
esm_path = jsm_path.parent / esm_name
|
|
return esm_path
|
|
|
|
|
|
path_to_uri_map = None
|
|
|
|
|
|
def load_path_to_uri_map():
|
|
global path_to_uri_map
|
|
|
|
if path_to_uri_map:
|
|
return
|
|
|
|
if "ESMIFY_MAP_JSON" in os.environ:
|
|
json_map = pathlib.Path(os.environ["ESMIFY_MAP_JSON"])
|
|
else:
|
|
json_map = pathlib.Path(__file__).parent / "map.json"
|
|
|
|
with open(json_map, "r") as f:
|
|
uri_to_path_map = json.loads(f.read())
|
|
|
|
path_to_uri_map = dict()
|
|
|
|
for uri, paths in uri_to_path_map.items():
|
|
if type(paths) is str:
|
|
paths = [paths]
|
|
|
|
for path in paths:
|
|
path_to_uri_map[path] = uri
|
|
|
|
|
|
def find_jsm_uri(jsm_path):
|
|
load_path_to_uri_map()
|
|
|
|
path = path_sep_from_native(jsm_path)
|
|
|
|
if path in path_to_uri_map:
|
|
return path_to_uri_map[path]
|
|
|
|
return None
|
|
|
|
|
|
def rename_single_file(command_context, vcs_utils, jsm_path, summary):
|
|
"""Rename `jsm_path` to .mjs, and fix references to the file in build
|
|
and test definitions."""
|
|
|
|
def info(text):
|
|
command_context.log(logging.INFO, "esmify", {}, f"[INFO] {text}")
|
|
|
|
esm_path = esmify_path(jsm_path)
|
|
|
|
jsm_name = jsm_path.name
|
|
esm_name = esm_path.name
|
|
|
|
target_files = []
|
|
|
|
info(f"{jsm_path} => {esm_path}")
|
|
|
|
renamed = False
|
|
for target in target_files:
|
|
if try_rename_in(
|
|
command_context, jsm_path, target, jsm_name, esm_name, jsm_path
|
|
):
|
|
renamed = True
|
|
|
|
if try_rename_components_conf(command_context, jsm_path, jsm_name, esm_name):
|
|
renamed = True
|
|
|
|
uri_target_files = [
|
|
pathlib.Path(
|
|
"browser", "base", "content", "test", "performance", "browser_startup.js"
|
|
),
|
|
pathlib.Path(
|
|
"browser",
|
|
"base",
|
|
"content",
|
|
"test",
|
|
"performance",
|
|
"browser_startup_content.js",
|
|
),
|
|
pathlib.Path(
|
|
"browser",
|
|
"base",
|
|
"content",
|
|
"test",
|
|
"performance",
|
|
"browser_startup_content_subframe.js",
|
|
),
|
|
pathlib.Path(
|
|
"toolkit",
|
|
"components",
|
|
"backgroundtasks",
|
|
"tests",
|
|
"browser",
|
|
"browser_xpcom_graph_wait.js",
|
|
),
|
|
]
|
|
|
|
jsm_uri = find_jsm_uri(jsm_path)
|
|
if jsm_uri:
|
|
esm_uri = re.sub(r"\.(jsm|js|jsm\.js)$", ".sys.mjs", jsm_uri)
|
|
|
|
for target in uri_target_files:
|
|
if try_rename_uri_in(
|
|
command_context, target, jsm_uri, esm_uri, jsm_name, esm_name
|
|
):
|
|
renamed = True
|
|
|
|
if not renamed:
|
|
summary.no_refs.append([jsm_path, None])
|
|
|
|
if not esm_path.exists():
|
|
vcs_utils.rename(jsm_path, esm_path)
|
|
else:
|
|
summary.rename_errors.append([jsm_path, f"{esm_path} already exists"])
|
|
|
|
return esm_path
|
|
|
|
|
|
def rename_jsms(command_context, vcs_utils, jsms, summary):
|
|
esms = []
|
|
for jsm in jsms:
|
|
esm = rename_single_file(command_context, vcs_utils, jsm, summary)
|
|
esms.append(esm)
|
|
|
|
return esms
|
|
|
|
|
|
def run_jscodeshift(script, env, stdin):
|
|
cmd = [
|
|
"npx",
|
|
"jscodeshift",
|
|
"-t",
|
|
str(npm_prefix / script),
|
|
"--stdin",
|
|
"--verbose=2",
|
|
'--extensions=js,jsx,mjs,jsm'
|
|
]
|
|
p = subprocess.Popen(cmd, env=env, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
|
|
p.stdin.write(stdin)
|
|
p.stdin.close()
|
|
|
|
ok_files = []
|
|
errors = []
|
|
while True:
|
|
line = p.stdout.readline()
|
|
if not line:
|
|
break
|
|
line = line.rstrip().decode()
|
|
|
|
if line.startswith(" NOC "):
|
|
continue
|
|
|
|
print(line)
|
|
|
|
m = re.search(r"^ (OKK|ERR) ([^ ]+)(?: (.+))?", line)
|
|
if not m:
|
|
continue
|
|
|
|
result = m.group(1)
|
|
path = pathlib.Path(m.group(2))
|
|
error = m.group(3)
|
|
|
|
if result == "OKK":
|
|
ok_files.append(path)
|
|
|
|
if result == "ERR":
|
|
errors.append([path, error])
|
|
|
|
if p.wait() != 0:
|
|
return [None, None]
|
|
|
|
return ok_files, errors
|
|
|
|
|
|
def eslint_fix(command_context, files):
|
|
"""Run ESLint with a very basic configuration to fix problems introduced by jscodeshift."""
|
|
|
|
def info(text):
|
|
command_context.log(logging.INFO, "migrate", {}, f"[INFO] {text}")
|
|
|
|
if len(files) == 0:
|
|
return
|
|
|
|
args = [
|
|
"npx",
|
|
"eslint",
|
|
"--no-config-lookup",
|
|
# jscodeshift adds extra parentheses in `new function () {...}` expressions,
|
|
# making them look like `new (function () {...})` -- undo that
|
|
"--rule",
|
|
"no-extra-parens: [error, functions]",
|
|
# jscodeshift doesn't leave a space before the opening paren in a function
|
|
"--rule",
|
|
"space-before-function-paren: [warn, { anonymous: always, asyncArrow: always, named: never }]",
|
|
"--fix",
|
|
]
|
|
|
|
remaining = files[0:]
|
|
|
|
# There can be too many files for single command line, perform by chunk.
|
|
max_files = 16
|
|
while len(remaining) > max_files:
|
|
info(f"{len(remaining)} files remaining")
|
|
|
|
chunk = remaining[0:max_files]
|
|
remaining = remaining[max_files:]
|
|
|
|
cmd = args + chunk
|
|
# Don't check status code or print output -- zotero-client has many unfixable errors
|
|
subprocess.run(cmd, stdout=subprocess.DEVNULL)
|
|
|
|
info(f"{len(remaining)} files remaining")
|
|
chunk = remaining
|
|
cmd = args + chunk
|
|
# Don't check status code or print output -- zotero-client has many unfixable errors
|
|
subprocess.run(cmd, stdout=subprocess.DEVNULL)
|
|
|
|
|
|
npm_prefix = pathlib.Path("scripts") / "migrate-fx140"
|
|
path_from_npm_prefix = pathlib.Path("..") / ".."
|
|
|
|
if __name__ == "__main__":
|
|
if not (npm_prefix / "node_modules").is_dir():
|
|
cmd = [
|
|
"npm",
|
|
"install",
|
|
"--prefix",
|
|
str(npm_prefix),
|
|
]
|
|
subprocess.run(cmd, check=True)
|
|
|
|
parser = argparse.ArgumentParser()
|
|
subparsers = parser.add_subparsers(required=True)
|
|
|
|
parser_esmify = subparsers.add_parser("esmify")
|
|
parser_esmify.set_defaults(func=esmify)
|
|
parser_esmify.add_argument(
|
|
"--convert",
|
|
action="store_true",
|
|
help="Only perform the step 1 = convert part",
|
|
)
|
|
parser_esmify.add_argument(
|
|
"--imports",
|
|
action="store_true",
|
|
help="Only perform the step 2 = import calls part",
|
|
)
|
|
parser_esmify.add_argument(
|
|
"--prefix",
|
|
default="",
|
|
help="Restrict the target of import in the step 2 to ESM-ified JSM, by the "
|
|
"prefix match for the JSM file's path. e.g. 'browser/'.",
|
|
)
|
|
|
|
parser_asyncify = subparsers.add_parser("asyncify")
|
|
parser_asyncify.set_defaults(func=asyncify)
|
|
|
|
parser.add_argument(
|
|
"path",
|
|
nargs=1,
|
|
help="Path to the JSM file to migrate, or the directory that contains "
|
|
"JSM files and/or JS files that reference ESM-ified JSM.",
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
func = args.func
|
|
delattr(args, 'func')
|
|
func(**vars(args))
|