tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

commit e96b05efb97b6fe259025952ba432ac2fa81a725
parent adf2646a51c67dfc0f1e4a2140ca3dd26dc5bf95
Author: Atila Butkovits <abutkovits@mozilla.com>
Date:   Tue,  9 Dec 2025 08:03:17 +0200

Revert "Bug 1967968, Bug 1704891 - Remove vendored `jsmin` package r=firefox-build-system-reviewers,mach-reviewers,ahal,nalexander" for causing mpu failures.

This reverts commit adf2646a51c67dfc0f1e4a2140ca3dd26dc5bf95.

Revert "Bug 1967968 - Use `Terser` instead of `JSMin` for Javascript minification r=calixte,marco,firefox-build-system-reviewers,Standard8,glandium"

This reverts commit 2ae8c75811315125f73923217532a57a58f928b1.

Revert "Bug 1967968 - Add `Terser` toolchain in CI r=firefox-build-system-reviewers,glandium,Standard8"

This reverts commit 499e44f544e4f2ce13a796f5905cabaae97f441f.

Revert "Bug 1704891 - Move checking for/setting up `node` executables to `nodeutil.py` r=Standard8,firefox-build-system-reviewers,perftest-reviewers,mozperftest-reviewers,sparky,glandium"

This reverts commit 0b95eab13cbc4f6daebe75b96cec26bdae4cc988.

Diffstat:
M.gitignore | 1-
M.hgignore | 1-
Mbuild/sparse-profiles/taskgraph | 2--
Mpython/mozbuild/mozbuild/nodeutil.py | 176-------------------------------------------------------------------------------
Mpython/mozbuild/mozpack/files.py | 186+++++++++++++++++++++++++------------------------------------------------------
Apython/mozbuild/mozpack/test/support/minify_js_verify.py | 15+++++++++++++++
Mpython/mozbuild/mozpack/test/test_files.py | 120+++++++++++++++++++++++--------------------------------------------------------
Mpython/mozperftest/mozperftest/test/browsertime/runner.py | 18++++++++++++++----
Mpython/mozperftest/mozperftest/test/noderunner.py | 8+++++---
Mpython/mozperftest/mozperftest/tests/test_browsertime.py | 18+++++++++++++++---
Mpython/sites/mach.txt | 1+
Mtaskcluster/kinds/artifact-build/kind.yml | 3---
Mtaskcluster/kinds/build-fat-aar/kind.yml | 1-
Mtaskcluster/kinds/build/kind.yml | 3---
Mtaskcluster/kinds/instrumented-build/kind.yml | 1-
Mtaskcluster/kinds/source-test/mozlint.yml | 3---
Mtaskcluster/kinds/source-test/python-android.yml | 1-
Mtaskcluster/kinds/source-test/python.yml | 3---
Mtaskcluster/kinds/toolchain/misc.yml | 15---------------
Dtaskcluster/scripts/misc/terser.sh | 22----------------------
Athird_party/python/jsmin/CHANGELOG.txt | 79+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Athird_party/python/jsmin/LICENSE.txt | 23+++++++++++++++++++++++
Athird_party/python/jsmin/MANIFEST.in | 1+
Athird_party/python/jsmin/PKG-INFO | 199+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Athird_party/python/jsmin/README.rst | 95+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Athird_party/python/jsmin/jsmin.egg-info/PKG-INFO | 199+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Athird_party/python/jsmin/jsmin.egg-info/SOURCES.txt | 15+++++++++++++++
Athird_party/python/jsmin/jsmin.egg-info/dependency_links.txt | 1+
Athird_party/python/jsmin/jsmin.egg-info/top_level.txt | 1+
Athird_party/python/jsmin/jsmin/__init__.py | 252+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Athird_party/python/jsmin/jsmin/__main__.py | 37+++++++++++++++++++++++++++++++++++++
Athird_party/python/jsmin/jsmin/test.py | 636+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Athird_party/python/jsmin/notes.txt | 3+++
Athird_party/python/jsmin/setup.cfg | 5+++++
Athird_party/python/jsmin/setup.py | 36++++++++++++++++++++++++++++++++++++
Mthird_party/python/pyproject.toml | 1+
Mthird_party/python/requirements.txt | 3+++
Mthird_party/python/uv.lock | 8++++++++
Mthird_party/python/uv.lock.hash | 4++--
Mtoolkit/moz.configure | 17+----------------
Atoolkit/mozapps/installer/js-compare-ast.js | 31+++++++++++++++++++++++++++++++
Mtoolkit/mozapps/installer/packager.mk | 1-
Mtoolkit/mozapps/installer/packager.py | 13+++++++------
Mtools/browsertime/mach_commands.py | 12+++++++-----
Mtools/lint/eslint/__init__.py | 4++--
Mtools/lint/eslint/setup_helper.py | 217+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
Mtools/lint/node-licenses.yml | 1-
Mtools/lint/node-licenses/__init__.py | 4++--
Mtools/lint/stylelint/__init__.py | 4++--
Mtools/moztreedocs/mach_commands.py | 3+--
Dtools/terser/package-lock.json | 120-------------------------------------------------------------------------------
Dtools/terser/package.json | 8--------
Mtools/ts/mach_commands.py | 3+--
53 files changed, 2007 insertions(+), 627 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -97,7 +97,6 @@ devtools/**/node_modules/ tools/browsertime/node_modules/ tools/lint/eslint/eslint-plugin-mozilla/node_modules/ tools/lint/stylelint/stylelint-plugin-mozilla/node_modules/ -tools/terser/node_modules/ browser/components/asrouter/node_modules/ browser/components/aboutwelcome/node_modules/ browser/extensions/newtab/node_modules/ diff --git a/.hgignore b/.hgignore @@ -90,7 +90,6 @@ ^tools/browsertime/node_modules/ ^tools/lint/eslint/eslint-plugin-mozilla/node_modules/ ^tools/lint/stylelint/stylelint-plugin-mozilla/node_modules/ -^tools/terser/node_modules/ ^browser/components/asrouter/node_modules/ ^browser/components/aboutwelcome/node_modules/ ^browser/extensions/newtab/node_modules/ diff --git a/build/sparse-profiles/taskgraph b/build/sparse-profiles/taskgraph @@ -44,8 +44,6 @@ path:browser/components/aboutwelcome/package.json path:browser/components/aboutwelcome/package-lock.json path:browser/components/asrouter/package.json path:browser/components/asrouter/package-lock.json -path:tools/terser/package.json -path:tools/terser/package-lock.json # for new-style try pushes path:try_task_config.json diff --git a/python/mozbuild/mozbuild/nodeutil.py b/python/mozbuild/mozbuild/nodeutil.py @@ -5,45 +5,14 @@ import os import platform import subprocess -import sys from mozboot.util import get_tools_dir from mozfile import which -from mozfile.mozfile import remove as mozfileremove from packaging.version import Version NODE_MIN_VERSION = Version("12.22.12") NPM_MIN_VERSION = Version("6.14.16") -NODE_MACHING_VERSION_NOT_FOUND_MESSAGE = """ -Could not find Node.js executable later than %s. - -Executing `mach bootstrap --no-system-changes` should -install a compatible version in ~/.mozbuild on most platforms. -""".strip() - -NPM_MACHING_VERSION_NOT_FOUND_MESSAGE = """ -Could not find npm executable later than %s. - -Executing `mach bootstrap --no-system-changes` should -install a compatible version in ~/.mozbuild on most platforms. -""".strip() - -NODE_NOT_FOUND_MESSAGE = """ -nodejs is either not installed or is installed to a non-standard path. - -Executing `mach bootstrap --no-system-changes` should -install a compatible version in ~/.mozbuild on most platforms. -""".strip() - -NPM_NOT_FOUND_MESSAGE = """ -Node Package Manager (npm) is either not installed or installed to a -non-standard path. - -Executing `mach bootstrap --no-system-changes` should -install a compatible version in ~/.mozbuild on most platforms. -""".strip() - def find_node_paths(): """Determines the possible paths for node executables. @@ -154,148 +123,3 @@ def find_executable(name, min_version, use_node_for_version_check=False): return None, None return exe, version.release - - -def check_node_executables_valid(): - node_path, version = find_node_executable() - if not node_path: - print(NODE_NOT_FOUND_MESSAGE) - return False - if not version: - print(NODE_MACHING_VERSION_NOT_FOUND_MESSAGE % NODE_MIN_VERSION) - return False - - npm_path, version = find_npm_executable() - if not npm_path: - print(NPM_NOT_FOUND_MESSAGE) - return False - if not version: - print(NPM_MACHING_VERSION_NOT_FOUND_MESSAGE % NPM_MIN_VERSION) - return False - - return True - - -def package_setup( - package_root, - package_name, - should_update=False, - should_clobber=False, - no_optional=False, - skip_logging=False, -): - """Ensure `package_name` at `package_root` is installed. - - When `should_update` is true, clobber, install, and produce a new - "package-lock.json" file. - - This populates `package_root/node_modules`. - - """ - orig_cwd = os.getcwd() - - if should_update: - should_clobber = True - - try: - # npm sometimes fails to respect cwd when it is run using check_call so - # we manually switch folders here instead. - os.chdir(package_root) - - if should_clobber: - remove_directory(os.path.join(package_root, "node_modules"), skip_logging) - - npm_path, _ = find_npm_executable() - if not npm_path: - return 1 - - node_path, _ = find_node_executable() - if not node_path: - return 1 - - extra_parameters = ["--loglevel=error"] - - if no_optional: - extra_parameters.append("--no-optional") - - package_lock_json_path = os.path.join(package_root, "package-lock.json") - - if should_update: - cmd = [npm_path, "install"] - mozfileremove(package_lock_json_path) - else: - cmd = [npm_path, "ci"] - - # On non-Windows, ensure npm is called via node, as node may not be in the - # path. - if platform.system() != "Windows": - cmd.insert(0, node_path) - - cmd.extend(extra_parameters) - - # Ensure that bare `node` and `npm` in scripts, including post-install scripts, finds the - # binary we're invoking with. Without this, it's easy for compiled extensions to get - # mismatched versions of the Node.js extension API. - path = os.environ.get("PATH", "").split(os.pathsep) - node_dir = os.path.dirname(node_path) - if node_dir not in path: - path = [node_dir] + path - - if not skip_logging: - print( - 'Installing %s for mach using "%s"...' % (package_name, " ".join(cmd)) - ) - result = call_process( - package_name, cmd, append_env={"PATH": os.pathsep.join(path)} - ) - - if not result: - return 1 - - bin_path = os.path.join(package_root, "node_modules", ".bin", package_name) - - if not skip_logging: - print("\n%s installed successfully!" % package_name) - print("\nNOTE: Your local %s binary is at %s\n" % (package_name, bin_path)) - - finally: - os.chdir(orig_cwd) - - -def remove_directory(path, skip_logging=False): - if not skip_logging: - print("Clobbering %s..." % path) - if sys.platform.startswith("win") and have_winrm(): - process = subprocess.Popen(["winrm", "-rf", path]) - process.wait() - else: - mozfileremove(path) - - -def call_process(name, cmd, cwd=None, append_env={}): - env = dict(os.environ) - env.update(append_env) - - try: - with open(os.devnull, "w") as fnull: - subprocess.check_call(cmd, cwd=cwd, stdout=fnull, env=env) - except subprocess.CalledProcessError: - if cwd: - print("\nError installing %s in the %s folder, aborting." % (name, cwd)) - else: - print("\nError installing %s, aborting." % name) - - return False - - return True - - -def have_winrm(): - # `winrm -h` should print 'winrm version ...' and exit 1 - try: - p = subprocess.Popen( - ["winrm.exe", "-h"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT - ) - return p.wait() == 1 and p.stdout.read().startswith("winrm") - except Exception: - return False diff --git a/python/mozbuild/mozpack/files.py b/python/mozbuild/mozpack/files.py @@ -3,26 +3,25 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. import bisect +import codecs import errno import inspect -import json import os import platform import shutil import stat import subprocess -import tempfile import uuid from collections import OrderedDict -from io import BytesIO +from io import BytesIO, StringIO from itertools import chain, takewhile -from pathlib import Path from tarfile import TarFile, TarInfo -from tempfile import mkstemp +from tempfile import NamedTemporaryFile, mkstemp + +from jsmin import JavascriptMinify import mozbuild.makeutil as makeutil import mozpack.path as mozpath -from mozbuild.nodeutil import package_setup from mozbuild.preprocessor import Preprocessor from mozbuild.util import FileAvoidWrite, ensure_unicode, memoize from mozpack.chrome.manifest import ManifestEntry, ManifestInterfaces @@ -758,135 +757,75 @@ class MinifiedCommentStripped(BaseFile): class MinifiedJavaScript(BaseFile): """ - Minify JavaScript files using Terser while preserving - class and function names for better debugging. + File class for minifying JavaScript files. """ - TERSER_CONFIG = { - "parse": { - "ecma": 2020, - "module": True, - }, - "compress": { - "unused": True, - "passes": 3, - "ecma": 2020, - }, - "mangle": { - "keep_classnames": True, # Preserve class names - "keep_fnames": True, # Preserve function names - }, - "format": { - "comments": "/@lic|webpackIgnore|@vite-ignore/i", - "ascii_only": True, - "ecma": 2020, - }, - "sourceMap": False, - } - - def __init__(self, file, filepath): - """ - Initialize with a BaseFile instance to minify. - """ + def __init__(self, file, verify_command=None): + assert isinstance(file, BaseFile) self._file = file - self._filepath = filepath - - def _minify_with_terser(self, source_content): - """ - Minify JavaScript content using Terser - """ - if len(source_content) == 0: - return source_content - - import buildconfig - - node_path = buildconfig.substs.get("NODEJS") - if not node_path: - errors.fatal("NODEJS not found in build configuration") - - topsrcdir = Path(buildconfig.topsrcdir) - - if os.environ.get("MOZ_AUTOMATION"): - fetches_terser = ( - Path(os.environ["MOZ_FETCHES_DIR"]) - / "terser" - / "node_modules" - / "terser" - / "bin" - / "terser" - ) - if fetches_terser.exists(): - terser_path = fetches_terser - else: - errors.fatal(f"Terser toolchain not found at {fetches_terser}.") - else: - terser_dir = topsrcdir / "tools" / "terser" - terser_path = terser_dir / "node_modules" / "terser" / "bin" / "terser" - - if not terser_path.exists(): - # Automatically set up node_modules if terser is not found - package_setup(str(terser_dir), "terser") + self._verify_command = verify_command - # Verify that terser is now available after setup - if not terser_path.exists(): - errors.fatal( - f"Terser is required for JavaScript minification but could not be installed at {terser_path}. " - "Package setup may have failed." - ) + def open(self): + output = StringIO() + minify = JavascriptMinify( + codecs.getreader("utf-8")(self._file.open()), output, quote_chars="'\"`" + ) + minify.minify() + output.seek(0) + output_source = output.getvalue().encode() + output = BytesIO(output_source) - terser_cmd = [node_path, str(terser_path)] + if not self._verify_command: + return output - with tempfile.TemporaryDirectory() as temp_dir: - temp_path = Path(temp_dir) - config_path = temp_path / "terser_config.json" - source_path = temp_path / "source.js" + input_source = self._file.open().read() - config_path.write_text(json.dumps(self.TERSER_CONFIG), encoding="utf-8") - source_path.write_bytes(source_content) + with NamedTemporaryFile("wb+") as fh1, NamedTemporaryFile("wb+") as fh2: + fh1.write(input_source) + fh2.write(output_source) + fh1.flush() + fh2.flush() try: - result = subprocess.run( - terser_cmd - + [ - source_path, - "--config-file", - config_path, - ], - capture_output=True, - check=False, + args = list(self._verify_command) + args.extend([fh1.name, fh2.name]) + subprocess.check_output( + args, stderr=subprocess.STDOUT, universal_newlines=True ) + except subprocess.CalledProcessError as e: + errors.warn( + "JS minification verification failed for %s:" + % (getattr(self._file, "path", "<unknown>")) + ) + # Prefix each line with "Warning:" so mozharness doesn't + # think these error messages are real errors. + for line in e.output.splitlines(): + errors.warn(line) - if result.returncode == 0: - return result.stdout - else: - error_msg = result.stderr.decode("utf-8", errors="ignore") - errors.error( - f"Terser minification failed for {self._filepath}: {error_msg}" - ) - return source_content - - except subprocess.SubprocessError as e: - errors.error(f"Error running Terser for {self._filepath}: {e}") - return source_content + return self._file.open() - def open(self): - """ - Return a file-like object with the minified content. - """ - source_content = self._file.open().read() - minified = self._minify_with_terser(source_content) - return BytesIO(minified) + return output class BaseFinder: - def __init__(self, base, minify=False, minify_js=False, minify_pdfjs=False): + def __init__( + self, base, minify=False, minify_js=False, minify_js_verify_command=None + ): """ Initializes the instance with a reference base directory. The optional minify argument specifies whether minification of code should occur. minify_js is an additional option to control minification - of JavaScript. It requires minify to be True. minify_pdfjs controls - minification of PDF.js files independently. + of JavaScript. It requires minify to be True. + + minify_js_verify_command can be used to optionally verify the results + of JavaScript minification. If defined, it is expected to be an iterable + that will constitute the first arguments to a called process which will + receive the filenames of the original and minified JavaScript files. + The invoked process can then verify the results. If minification is + rejected, the process exits with a non-0 exit code and the original + JavaScript source is used. An example value for this argument is + ('/path/to/js', '/path/to/verify/script.js'). """ if minify_js and not minify: raise ValueError("minify_js requires minify.") @@ -894,7 +833,7 @@ class BaseFinder: self.base = mozpath.normsep(base) self._minify = minify self._minify_js = minify_js - self._minify_pdfjs = minify_pdfjs + self._minify_js_verify_command = minify_js_verify_command def find(self, pattern): """ @@ -958,17 +897,8 @@ class BaseFinder: if path.endswith((".ftl", ".properties")): return MinifiedCommentStripped(file) - if path.endswith((".js", ".jsm", ".mjs")): - file_path = mozpath.normsep(path) - filename = mozpath.basename(file_path) - # Don't minify prefs files because they use a custom parser that's stricter than JS - if filename.endswith("prefs.js") or "/defaults/pref" in file_path: - return file - # PDF.js files are minified based on the minify_pdfjs flag (for now) - if "pdfjs" in file_path and self._minify_pdfjs: - return MinifiedJavaScript(file, path) - elif self._minify_js: - return MinifiedJavaScript(file, path) + if self._minify_js and path.endswith((".js", ".jsm", ".mjs")): + return MinifiedJavaScript(file, self._minify_js_verify_command) return file @@ -1009,7 +939,7 @@ class FileFinder(BaseFinder): ignore=(), ignore_broken_symlinks=False, find_dotfiles=False, - **kargs, + **kargs ): """ Create a FileFinder for files under the given base directory. diff --git a/python/mozbuild/mozpack/test/support/minify_js_verify.py b/python/mozbuild/mozpack/test/support/minify_js_verify.py @@ -0,0 +1,15 @@ +# 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 sys + +if len(sys.argv) != 4: + raise Exception("Usage: minify_js_verify <exitcode> <orig> <minified>") + +retcode = int(sys.argv[1]) + +if retcode: + print("Error message", file=sys.stderr) + +sys.exit(retcode) diff --git a/python/mozbuild/mozpack/test/test_files.py b/python/mozbuild/mozpack/test/test_files.py @@ -2,12 +2,9 @@ # 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 buildconfig - from mozbuild.dirutils import ensureParentDir -from mozbuild.nodeutil import find_node_executable from mozbuild.util import ensure_bytes -from mozpack.errors import ErrorMessage +from mozpack.errors import ErrorMessage, errors from mozpack.files import ( AbsoluteSymlinkFile, ComposedFinder, @@ -41,7 +38,7 @@ import random import sys import tarfile import unittest -from io import BytesIO +from io import BytesIO, StringIO from tempfile import mkdtemp import mozfile @@ -894,91 +891,44 @@ class TestMinifiedJavaScript(TestWithTmpDir): "// Another comment", ] - def setUp(self): - super().setUp() - if not buildconfig.substs.get("NODEJS"): - node_exe, _ = find_node_executable() - if node_exe: - buildconfig.substs["NODEJS"] = node_exe - def test_minified_javascript(self): - """Test that MinifiedJavaScript minifies JavaScript content.""" - orig_f = GeneratedFile("\n".join(self.orig_lines).encode()) - min_f = MinifiedJavaScript(orig_f, "test.js") - - mini_content = min_f.open().read() - orig_content = orig_f.open().read() - - # Verify minification occurred (content should be smaller) - self.assertTrue(len(mini_content) < len(orig_content)) - # Verify content is not empty - self.assertTrue(len(mini_content) > 0) - - def test_minified_javascript_open(self): - """Test that MinifiedJavaScript.open returns appropriately reset file object.""" - orig_f = GeneratedFile("\n".join(self.orig_lines).encode()) - min_f = MinifiedJavaScript(orig_f, "test.js") - - # Test reading partial content - first_read = min_f.open().read(10) - self.assertTrue(len(first_read) <= 10) - - # Test reading full content multiple times - full_content = min_f.open().read() - second_read = min_f.open().read() - self.assertEqual(full_content, second_read) - - def test_preserves_functionality(self): - """Test that Terser preserves JavaScript functionality.""" - # More complex JavaScript with functions and objects - complex_js = """ - // This is a test function - function testFunction(param) { - let result = { - value: param * 2, - toString: function() { - return "Result: " + this.value; - } - }; - return result; - } - - // Export for testing - var exported = testFunction; - """ - - orig_f = GeneratedFile(complex_js.encode()) - min_f = MinifiedJavaScript(orig_f, "complex.js") - - minified_content = min_f.open().read().decode() - - # Verify it's minified - self.assertTrue(len(minified_content) < len(complex_js)) - # Verify functions are still present) - self.assertIn("function", minified_content) - - def test_handles_empty_file(self): - """Test that MinifiedJavaScript handles empty files gracefully.""" - empty_f = GeneratedFile(b"") - min_f = MinifiedJavaScript(empty_f, "empty.js") - - # Should handle empty content gracefully - result = min_f.open().read() - self.assertEqual(result, b"") + orig_f = GeneratedFile("\n".join(self.orig_lines)) + min_f = MinifiedJavaScript(orig_f) + + mini_lines = min_f.open().readlines() + self.assertTrue(mini_lines) + self.assertTrue(len(mini_lines) < len(self.orig_lines)) + + def _verify_command(self, code): + our_dir = os.path.abspath(os.path.dirname(__file__)) + return [ + sys.executable, + os.path.join(our_dir, "support", "minify_js_verify.py"), + code, + ] - def test_handles_syntax_errors(self): - """Test that MinifiedJavaScript raises an error for syntax errors.""" - # JavaScript with syntax error - broken_js = b"function broken( { return 'missing parenthesis'; }" + def test_minified_verify_success(self): + orig_f = GeneratedFile("\n".join(self.orig_lines)) + min_f = MinifiedJavaScript(orig_f, verify_command=self._verify_command("0")) - orig_f = GeneratedFile(broken_js) - min_f = MinifiedJavaScript(orig_f, "broken.js") + mini_lines = [s.decode() for s in min_f.open().readlines()] + self.assertTrue(mini_lines) + self.assertTrue(len(mini_lines) < len(self.orig_lines)) - # Should raise an ErrorMessage when minification fails - from mozpack.errors import ErrorMessage + def test_minified_verify_failure(self): + orig_f = GeneratedFile("\n".join(self.orig_lines)) + errors.out = StringIO() + min_f = MinifiedJavaScript(orig_f, verify_command=self._verify_command("1")) - with self.assertRaises(ErrorMessage): - min_f.open().read() + mini_lines = min_f.open().readlines() + output = errors.out.getvalue() + errors.out = sys.stderr + self.assertEqual( + output, + "warning: JS minification verification failed for <unknown>:\n" + "warning: Error message\n", + ) + self.assertEqual(mini_lines, orig_f.open().readlines()) class MatchTestTemplate: diff --git a/python/mozperftest/mozperftest/test/browsertime/runner.py b/python/mozperftest/mozperftest/test/browsertime/runner.py @@ -7,6 +7,7 @@ import os import pathlib import re import shutil +import sys from pathlib import Path from mozperftest.test.browsertime.visualtools import get_dependencies, xvfb @@ -93,9 +94,20 @@ class BrowsertimeRunner(NodeRunner): self.virtualenv_manager = mach_cmd.virtualenv_manager self._created_dirs = [] self._test_script = None + self._setup_helper = None self.get_binary_path = mach_cmd.get_binary_path @property + def setup_helper(self): + if self._setup_helper is not None: + return self._setup_helper + sys.path.append(str(Path(self.topsrcdir, "tools", "lint", "eslint"))) + import setup_helper + + self._setup_helper = setup_helper + return self._setup_helper + + @property def artifact_cache_path(self): """Downloaded artifacts will be kept here.""" # The convention is $MOZBUILD_STATE_PATH/cache/$FEATURE. @@ -240,9 +252,7 @@ class BrowsertimeRunner(NodeRunner): def _setup_node_packages(self, package_json_path): # Install the browsertime Node.js requirements. - from mozbuild.nodeutil import check_node_executables_valid, package_setup - - if not check_node_executables_valid(): + if not self.setup_helper.check_node_executables_valid(): return should_clobber = self.get_arg("clobber") @@ -261,7 +271,7 @@ class BrowsertimeRunner(NodeRunner): ) install_url = self.get_arg("install-url") - package_setup( + self.setup_helper.package_setup( str(self.state_path), "browsertime", should_update=install_url is not None, diff --git a/python/mozperftest/mozperftest/test/noderunner.py b/python/mozperftest/mozperftest/test/noderunner.py @@ -2,6 +2,7 @@ # 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 os +import sys import mozpack.path as mozpath @@ -61,13 +62,14 @@ class NodeRunner(Layer): def verify_node_install(self): # check if Node is installed - from mozbuild.nodeutil import check_node_executables_valid + sys.path.append(mozpath.join(self.topsrcdir, "tools", "lint", "eslint")) + import setup_helper with silence(): - node_valid = check_node_executables_valid() + node_valid = setup_helper.check_node_executables_valid() if not node_valid: # running again to get details printed out - check_node_executables_valid() + setup_helper.check_node_executables_valid() raise ValueError("Can't find Node. did you run ./mach bootstrap ?") return True diff --git a/python/mozperftest/mozperftest/tests/test_browsertime.py b/python/mozperftest/mozperftest/tests/test_browsertime.py @@ -232,6 +232,13 @@ def test_browser_desktop(*mocked): try: with sys as s, browser as b, silence(): + # just checking that the setup_helper property gets + # correctly initialized + browsertime = browser.layers[-1] + assert browsertime.setup_helper is not None + helper = browsertime.setup_helper + assert browsertime.setup_helper is helper + b(s(metadata)) finally: shutil.rmtree(mach_cmd._mach_context.state_dir) @@ -261,6 +268,13 @@ def test_existing_results(*mocked): try: with sys as s, browser as b, silence(): + # just checking that the setup_helper property gets + # correctly initialized + browsertime = browser.layers[-1] + assert browsertime.setup_helper is not None + helper = browsertime.setup_helper + assert browsertime.setup_helper is helper + m = b(s(metadata)) results = m.get_results() assert len(results) == 1 @@ -286,9 +300,7 @@ def test_add_options(): "mozperftest.test.noderunner.NodeRunner.verify_node_install", new=lambda x: True ) @mock.patch("mozbuild.artifact_cache.ArtifactCache.fetch", new=fetch) -@mock.patch( - "mozperftest.test.browsertime.runner.BrowsertimeRunner._setup_node_packages" -) +@mock.patch("mozperftest.test.browsertime.runner.BrowsertimeRunner.setup_helper") def test_install_url(*mocked): url = "https://here/tarball/" + "".join( [random.choice(string.hexdigits[:-6]) for c in range(40)] diff --git a/python/sites/mach.txt b/python/sites/mach.txt @@ -85,6 +85,7 @@ vendored:third_party/python/idna vendored:third_party/python/importlib_metadata vendored:third_party/python/jinja2 vendored:third_party/python/jinxed +vendored:third_party/python/jsmin vendored:third_party/python/jsonschema vendored:third_party/python/looseversion vendored:third_party/python/markupsafe/src diff --git a/taskcluster/kinds/artifact-build/kind.yml b/taskcluster/kinds/artifact-build/kind.yml @@ -42,9 +42,6 @@ task-defaults: tooltool-downloads: public keep-artifacts: false use-python: default - fetches: - toolchain: - - terser tasks: linux64-artifact/opt: diff --git a/taskcluster/kinds/build-fat-aar/kind.yml b/taskcluster/kinds/build-fat-aar/kind.yml @@ -111,7 +111,6 @@ task-defaults: - linux64-node - sysroot-x86_64-linux-gnu - sysroot-wasm32-wasi - - terser armeabi-v7a: - artifact: target.maven.zip dest: armeabi-v7a diff --git a/taskcluster/kinds/build/kind.yml b/taskcluster/kinds/build/kind.yml @@ -37,9 +37,6 @@ task-defaults: MACH_BUILD_PYTHON_NATIVE_PACKAGE_SOURCE: system use-python: default use-uv: true - fetches: - toolchain: - - terser run: mozconfig-variant: by-release-type: diff --git a/taskcluster/kinds/instrumented-build/kind.yml b/taskcluster/kinds/instrumented-build/kind.yml @@ -42,7 +42,6 @@ task-defaults: fetches: toolchain: - linux64-sccache - - terser tasks: linux64-shippable/opt: diff --git a/taskcluster/kinds/source-test/mozlint.yml b/taskcluster/kinds/source-test/mozlint.yml @@ -327,7 +327,6 @@ node-licenses: ln -s ../tools/lint/eslint/eslint-plugin-spidermonkey-js node_modules && cp -r $MOZ_FETCHES_DIR/eslint-plugin-mozilla/node_modules tools/lint/eslint/eslint-plugin-mozilla/node_modules && cp -r $MOZ_FETCHES_DIR/stylelint-plugin-mozilla/node_modules tools/lint/stylelint/stylelint-plugin-mozilla/node_modules && - cp -r $MOZ_FETCHES_DIR/terser/node_modules tools/terser/node_modules && ./mach lint -v -l node-licenses -f treeherder -f json:/builds/worker/mozlint.json . when: files-changed: @@ -339,14 +338,12 @@ node-licenses: - package.json - tools/lint/eslint/eslint-plugin-mozilla/package.json - tools/lint/stylelint/stylelint-plugin-mozilla/package.json - - tools/terser/package.json fetches: toolchain: - linux64-node - node-modules - eslint-plugin-mozilla - stylelint-plugin-mozilla - - terser node-package-names: description: Lint for node package name issues diff --git a/taskcluster/kinds/source-test/python-android.yml b/taskcluster/kinds/source-test/python-android.yml @@ -42,7 +42,6 @@ android-gradle-build: - linux64-android-sdk-linux-repack - linux64-jdk-repack - linux64-node - - terser when: files-changed: # Build stuff. diff --git a/taskcluster/kinds/source-test/python.yml b/taskcluster/kinds/source-test/python.yml @@ -393,14 +393,11 @@ mozbuild: by-platform: linux2404-64/opt: - linux64-node - - terser macosx1470-64/opt: - macosx64-node - - terser windows11-64/opt: - win64-node - win64-mozmake - - terser when: files-changed: - '**/moz.configure' diff --git a/taskcluster/kinds/toolchain/misc.yml b/taskcluster/kinds/toolchain/misc.yml @@ -473,21 +473,6 @@ stylelint-plugin-mozilla: toolchain: - linux64-node -terser: - description: "npm install terser node_modules" - treeherder: - symbol: TL(terser) - run: - script: terser.sh - sparse-profile: null - resources: - - 'tools/terser/package.json' - - 'tools/terser/package-lock.json' - toolchain-artifact: public/build/terser.tar.zst - fetches: - toolchain: - - linux64-node - newtab-node-modules: description: "npm install newtab node_modules" treeherder: diff --git a/taskcluster/scripts/misc/terser.sh b/taskcluster/scripts/misc/terser.sh @@ -1,21 +0,0 @@ -#!/bin/bash -vex - -set -x -e - -echo "running as" $(id) - -set -v - -cd $GECKO_PATH - -export PATH=$PATH:$MOZ_FETCHES_DIR/node/bin - -./mach configure --disable-compile-environment -./mach npm ci --prefix tools/terser - -# We have tools/terser/{node_modules,...} and want -# terser/{node_modules}. -cd tools/ -tar caf /tmp/terser.tar.zst terser -mkdir -p /builds/worker/artifacts -mv /tmp/terser.tar.zst /builds/worker/artifacts/ -\ No newline at end of file diff --git a/third_party/python/jsmin/CHANGELOG.txt b/third_party/python/jsmin/CHANGELOG.txt @@ -0,0 +1,79 @@ +Changelog +========= + +v3.0.0 (2021-09-08) Ben Bradshaw +-------------------------------- + +- Breaking Change: Removed support for Python 2 + +- Removed usage of use_2to3 in setup.py + +v2.2.2 (2017-05-01) Tikitu de Jager +----------------------------------- + +- Add license headers to code files (fixes i#17) + +- Remove mercurial files (fixes #20) + +v2.2.1 (2016-03-06) Tikitu de Jager +----------------------------------- + +- Fix #14: Infinite loop on `return x / 1;` + +v2.2.0 (2015-12-19) Tikitu de Jager +----------------------------------- + +- Merge #13: Preserve "loud comments" starting with `/*!` + + These are commonly used for copyright notices, and are preserved by various + other minifiers (e.g. YUI Compressor). + +v2.1.6 (2015-10-14) Tikitu de Jager +----------------------------------- + +- Fix #12: Newline following a regex literal should not be elided. + +v2.1.5 (2015-10-11) Tikitu de Jager +----------------------------------- + +- Fix #9: Premature end of statement caused by multi-line comment not + adding newline. + +- Fix #10: Removing multiline comment separating tokens must leave a space. + +- Refactor comment handling for maintainability. + +v2.1.4 (2015-08-23) Tikitu de Jager +----------------------------------- + +- Fix #6: regex literal matching comment was not correctly matched. + +- Refactor regex literal handling for robustness. + +v2.1.3 (2015-08-09) Tikitu de Jager +----------------------------------- + +- Reset issue numbering: issues live in github from now on. + +- Fix #1: regex literal was not recognised when occurring directly after `{`. + +v2.1.2 (2015-07-12) Tikitu de Jager +----------------------------------- + +- Issue numbers here and below refer to the bitbucket repository. + +- Fix #17: bug when JS starts with comment then literal regex. + +v2.1.1 (2015-02-14) Tikitu de Jager +----------------------------------- + +- Fix #16: bug returning a literal regex containing escaped forward-slashes. + +v2.1.0 (2014-12-24) Tikitu de Jager +----------------------------------- + +- First changelog entries; see README.rst for prior contributors. + +- Expose quote_chars parameter to provide just enough unofficial Harmony + support to be useful. + diff --git a/third_party/python/jsmin/LICENSE.txt b/third_party/python/jsmin/LICENSE.txt @@ -0,0 +1,23 @@ +The MIT License (MIT) + +Copyright (c) 2013 Dave St.Germain + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + diff --git a/third_party/python/jsmin/MANIFEST.in b/third_party/python/jsmin/MANIFEST.in @@ -0,0 +1 @@ +include *.txt diff --git a/third_party/python/jsmin/PKG-INFO b/third_party/python/jsmin/PKG-INFO @@ -0,0 +1,199 @@ +Metadata-Version: 2.1 +Name: jsmin +Version: 3.0.1 +Summary: JavaScript minifier. +Home-page: https://github.com/tikitu/jsmin/ +Author: Dave St.Germain +Author-email: dave@st.germa.in +Maintainer: Tikitu de Jager +Maintainer-email: tikitu+jsmin@logophile.org +License: MIT License +Platform: UNKNOWN +Classifier: Development Status :: 5 - Production/Stable +Classifier: Environment :: Web Environment +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: MIT License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python :: 3 :: Only +Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content +Classifier: Topic :: Software Development :: Pre-processors +Classifier: Topic :: Text Processing :: Filters +License-File: LICENSE.txt + +===== +jsmin +===== + +JavaScript minifier. + +Usage +===== + +.. code:: python + + from jsmin import jsmin + with open('myfile.js') as js_file: + minified = jsmin(js_file.read()) + +You can run it as a commandline tool also:: + + python -m jsmin myfile.js + +NB: ``jsmin`` makes no attempt to be compatible with +`ECMAScript 6 / ES.next / Harmony <http://wiki.ecmascript.org/doku.php?id=harmony:specification_drafts>`_. +The current maintainer does not intend to add ES6-compatibility. If you would +like to take over maintenance and update ``jsmin`` for ES6, please contact +`Tikitu de Jager <mailto:tikitu+jsmin@logophile.org>`_. Pull requests are also +welcome, of course, but my time to review them is somewhat limited these days. + +If you're using ``jsmin`` on ES6 code, though, you might find the ``quote_chars`` +parameter useful: + +.. code:: python + + from jsmin import jsmin + with open('myfile.js') as js_file: + minified = jsmin(js_file.read(), quote_chars="'\"`") + + +Where to get it +=============== + +* install the package `from pypi <https://pypi.python.org/pypi/jsmin/>`_ +* get the latest release `from latest-release on github <https://github.com/tikitu/jsmin/tree/latest-release/jsmin>`_ +* get the development version `from master on github <https://github.com/tikitu/jsmin/>`_ + + +Python 2 support removed +======================== + +Python 2 support was removed in version 3.0.0. If you need to support Python 2, please use version 2.2.2 with setuptools<58. + +Contributing +============ + +`Issues <https://github.com/tikitu/jsmin/issues>`_ and `Pull requests <https://github.com/tikitu/jsmin/pulls>`_ +will be gratefully received on Github. The project used to be hosted +`on bitbucket <https://bitbucket.org/dcs/jsmin/>`_ and old issues can still be +found there. + +If possible, please make separate pull requests for tests and for code: tests will be added to the `latest-release` branch while code will go to `master`. + +Unless you request otherwise, your Github identity will be added to the contributor's list below; if you prefer a +different name feel free to add it in your pull request instead. (If you prefer not to be mentioned you'll have to let +the maintainer know somehow.) + +Build/test status +================= + +Both branches are tested with Travis: https://travis-ci.org/tikitu/jsmin + +The `latest-release` branch (the version on PyPI plus any new tests) is tested against CPython 3. +Currently: + +.. image:: https://travis-ci.org/tikitu/jsmin.png?branch=latest-release + +If that branch is failing that means there's a new test that fails on *the latest released version on pypi*, with no fix yet +released. + +The `master` branch (development version, might be ahead of latest released version) is tested against CPython 3. +Currently: + +.. image:: https://travis-ci.org/tikitu/jsmin.png?branch=master + +If `master` is failing don't use it, but as long as `latest-release` is passing the pypi release should be ok. + +Contributors (chronological commit order) +========================================= + +* `Dave St.Germain <https://bitbucket.org/dcs>`_ (original author) +* `Hans weltar <https://bitbucket.org/hansweltar>`_ +* `Tikitu de Jager <mailto:tikitu+jsmin@logophile.org>`_ (current maintainer) +* https://bitbucket.org/rennat +* `Nick Alexander <https://bitbucket.org/ncalexan>`_ +* `Gennady Kovshenin <https://github.com/soulseekah>`_ +* `Matt Molyneaux <https://github.com/moggers87>`_ +* `Albert Wang <https://github.com/albertyw>`_ +* `Ben Bradshaw <https://github.com/serenecloud>`_ + +Changelog +========= + +v3.0.0 (2021-09-08) Ben Bradshaw +-------------------------------- + +- Breaking Change: Removed support for Python 2 + +- Removed usage of use_2to3 in setup.py + +v2.2.2 (2017-05-01) Tikitu de Jager +----------------------------------- + +- Add license headers to code files (fixes i#17) + +- Remove mercurial files (fixes #20) + +v2.2.1 (2016-03-06) Tikitu de Jager +----------------------------------- + +- Fix #14: Infinite loop on `return x / 1;` + +v2.2.0 (2015-12-19) Tikitu de Jager +----------------------------------- + +- Merge #13: Preserve "loud comments" starting with `/*!` + + These are commonly used for copyright notices, and are preserved by various + other minifiers (e.g. YUI Compressor). + +v2.1.6 (2015-10-14) Tikitu de Jager +----------------------------------- + +- Fix #12: Newline following a regex literal should not be elided. + +v2.1.5 (2015-10-11) Tikitu de Jager +----------------------------------- + +- Fix #9: Premature end of statement caused by multi-line comment not + adding newline. + +- Fix #10: Removing multiline comment separating tokens must leave a space. + +- Refactor comment handling for maintainability. + +v2.1.4 (2015-08-23) Tikitu de Jager +----------------------------------- + +- Fix #6: regex literal matching comment was not correctly matched. + +- Refactor regex literal handling for robustness. + +v2.1.3 (2015-08-09) Tikitu de Jager +----------------------------------- + +- Reset issue numbering: issues live in github from now on. + +- Fix #1: regex literal was not recognised when occurring directly after `{`. + +v2.1.2 (2015-07-12) Tikitu de Jager +----------------------------------- + +- Issue numbers here and below refer to the bitbucket repository. + +- Fix #17: bug when JS starts with comment then literal regex. + +v2.1.1 (2015-02-14) Tikitu de Jager +----------------------------------- + +- Fix #16: bug returning a literal regex containing escaped forward-slashes. + +v2.1.0 (2014-12-24) Tikitu de Jager +----------------------------------- + +- First changelog entries; see README.rst for prior contributors. + +- Expose quote_chars parameter to provide just enough unofficial Harmony + support to be useful. + + + diff --git a/third_party/python/jsmin/README.rst b/third_party/python/jsmin/README.rst @@ -0,0 +1,95 @@ +===== +jsmin +===== + +JavaScript minifier. + +Usage +===== + +.. code:: python + + from jsmin import jsmin + with open('myfile.js') as js_file: + minified = jsmin(js_file.read()) + +You can run it as a commandline tool also:: + + python -m jsmin myfile.js + +NB: ``jsmin`` makes no attempt to be compatible with +`ECMAScript 6 / ES.next / Harmony <http://wiki.ecmascript.org/doku.php?id=harmony:specification_drafts>`_. +The current maintainer does not intend to add ES6-compatibility. If you would +like to take over maintenance and update ``jsmin`` for ES6, please contact +`Tikitu de Jager <mailto:tikitu+jsmin@logophile.org>`_. Pull requests are also +welcome, of course, but my time to review them is somewhat limited these days. + +If you're using ``jsmin`` on ES6 code, though, you might find the ``quote_chars`` +parameter useful: + +.. code:: python + + from jsmin import jsmin + with open('myfile.js') as js_file: + minified = jsmin(js_file.read(), quote_chars="'\"`") + + +Where to get it +=============== + +* install the package `from pypi <https://pypi.python.org/pypi/jsmin/>`_ +* get the latest release `from latest-release on github <https://github.com/tikitu/jsmin/tree/latest-release/jsmin>`_ +* get the development version `from master on github <https://github.com/tikitu/jsmin/>`_ + + +Python 2 support removed +======================== + +Python 2 support was removed in version 3.0.0. If you need to support Python 2, please use version 2.2.2 with setuptools<58. + +Contributing +============ + +`Issues <https://github.com/tikitu/jsmin/issues>`_ and `Pull requests <https://github.com/tikitu/jsmin/pulls>`_ +will be gratefully received on Github. The project used to be hosted +`on bitbucket <https://bitbucket.org/dcs/jsmin/>`_ and old issues can still be +found there. + +If possible, please make separate pull requests for tests and for code: tests will be added to the `latest-release` branch while code will go to `master`. + +Unless you request otherwise, your Github identity will be added to the contributor's list below; if you prefer a +different name feel free to add it in your pull request instead. (If you prefer not to be mentioned you'll have to let +the maintainer know somehow.) + +Build/test status +================= + +Both branches are tested with Travis: https://travis-ci.org/tikitu/jsmin + +The `latest-release` branch (the version on PyPI plus any new tests) is tested against CPython 3. +Currently: + +.. image:: https://travis-ci.org/tikitu/jsmin.png?branch=latest-release + +If that branch is failing that means there's a new test that fails on *the latest released version on pypi*, with no fix yet +released. + +The `master` branch (development version, might be ahead of latest released version) is tested against CPython 3. +Currently: + +.. image:: https://travis-ci.org/tikitu/jsmin.png?branch=master + +If `master` is failing don't use it, but as long as `latest-release` is passing the pypi release should be ok. + +Contributors (chronological commit order) +========================================= + +* `Dave St.Germain <https://bitbucket.org/dcs>`_ (original author) +* `Hans weltar <https://bitbucket.org/hansweltar>`_ +* `Tikitu de Jager <mailto:tikitu+jsmin@logophile.org>`_ (current maintainer) +* https://bitbucket.org/rennat +* `Nick Alexander <https://bitbucket.org/ncalexan>`_ +* `Gennady Kovshenin <https://github.com/soulseekah>`_ +* `Matt Molyneaux <https://github.com/moggers87>`_ +* `Albert Wang <https://github.com/albertyw>`_ +* `Ben Bradshaw <https://github.com/serenecloud>`_ diff --git a/third_party/python/jsmin/jsmin.egg-info/PKG-INFO b/third_party/python/jsmin/jsmin.egg-info/PKG-INFO @@ -0,0 +1,199 @@ +Metadata-Version: 2.1 +Name: jsmin +Version: 3.0.1 +Summary: JavaScript minifier. +Home-page: https://github.com/tikitu/jsmin/ +Author: Dave St.Germain +Author-email: dave@st.germa.in +Maintainer: Tikitu de Jager +Maintainer-email: tikitu+jsmin@logophile.org +License: MIT License +Platform: UNKNOWN +Classifier: Development Status :: 5 - Production/Stable +Classifier: Environment :: Web Environment +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: MIT License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python :: 3 :: Only +Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content +Classifier: Topic :: Software Development :: Pre-processors +Classifier: Topic :: Text Processing :: Filters +License-File: LICENSE.txt + +===== +jsmin +===== + +JavaScript minifier. + +Usage +===== + +.. code:: python + + from jsmin import jsmin + with open('myfile.js') as js_file: + minified = jsmin(js_file.read()) + +You can run it as a commandline tool also:: + + python -m jsmin myfile.js + +NB: ``jsmin`` makes no attempt to be compatible with +`ECMAScript 6 / ES.next / Harmony <http://wiki.ecmascript.org/doku.php?id=harmony:specification_drafts>`_. +The current maintainer does not intend to add ES6-compatibility. If you would +like to take over maintenance and update ``jsmin`` for ES6, please contact +`Tikitu de Jager <mailto:tikitu+jsmin@logophile.org>`_. Pull requests are also +welcome, of course, but my time to review them is somewhat limited these days. + +If you're using ``jsmin`` on ES6 code, though, you might find the ``quote_chars`` +parameter useful: + +.. code:: python + + from jsmin import jsmin + with open('myfile.js') as js_file: + minified = jsmin(js_file.read(), quote_chars="'\"`") + + +Where to get it +=============== + +* install the package `from pypi <https://pypi.python.org/pypi/jsmin/>`_ +* get the latest release `from latest-release on github <https://github.com/tikitu/jsmin/tree/latest-release/jsmin>`_ +* get the development version `from master on github <https://github.com/tikitu/jsmin/>`_ + + +Python 2 support removed +======================== + +Python 2 support was removed in version 3.0.0. If you need to support Python 2, please use version 2.2.2 with setuptools<58. + +Contributing +============ + +`Issues <https://github.com/tikitu/jsmin/issues>`_ and `Pull requests <https://github.com/tikitu/jsmin/pulls>`_ +will be gratefully received on Github. The project used to be hosted +`on bitbucket <https://bitbucket.org/dcs/jsmin/>`_ and old issues can still be +found there. + +If possible, please make separate pull requests for tests and for code: tests will be added to the `latest-release` branch while code will go to `master`. + +Unless you request otherwise, your Github identity will be added to the contributor's list below; if you prefer a +different name feel free to add it in your pull request instead. (If you prefer not to be mentioned you'll have to let +the maintainer know somehow.) + +Build/test status +================= + +Both branches are tested with Travis: https://travis-ci.org/tikitu/jsmin + +The `latest-release` branch (the version on PyPI plus any new tests) is tested against CPython 3. +Currently: + +.. image:: https://travis-ci.org/tikitu/jsmin.png?branch=latest-release + +If that branch is failing that means there's a new test that fails on *the latest released version on pypi*, with no fix yet +released. + +The `master` branch (development version, might be ahead of latest released version) is tested against CPython 3. +Currently: + +.. image:: https://travis-ci.org/tikitu/jsmin.png?branch=master + +If `master` is failing don't use it, but as long as `latest-release` is passing the pypi release should be ok. + +Contributors (chronological commit order) +========================================= + +* `Dave St.Germain <https://bitbucket.org/dcs>`_ (original author) +* `Hans weltar <https://bitbucket.org/hansweltar>`_ +* `Tikitu de Jager <mailto:tikitu+jsmin@logophile.org>`_ (current maintainer) +* https://bitbucket.org/rennat +* `Nick Alexander <https://bitbucket.org/ncalexan>`_ +* `Gennady Kovshenin <https://github.com/soulseekah>`_ +* `Matt Molyneaux <https://github.com/moggers87>`_ +* `Albert Wang <https://github.com/albertyw>`_ +* `Ben Bradshaw <https://github.com/serenecloud>`_ + +Changelog +========= + +v3.0.0 (2021-09-08) Ben Bradshaw +-------------------------------- + +- Breaking Change: Removed support for Python 2 + +- Removed usage of use_2to3 in setup.py + +v2.2.2 (2017-05-01) Tikitu de Jager +----------------------------------- + +- Add license headers to code files (fixes i#17) + +- Remove mercurial files (fixes #20) + +v2.2.1 (2016-03-06) Tikitu de Jager +----------------------------------- + +- Fix #14: Infinite loop on `return x / 1;` + +v2.2.0 (2015-12-19) Tikitu de Jager +----------------------------------- + +- Merge #13: Preserve "loud comments" starting with `/*!` + + These are commonly used for copyright notices, and are preserved by various + other minifiers (e.g. YUI Compressor). + +v2.1.6 (2015-10-14) Tikitu de Jager +----------------------------------- + +- Fix #12: Newline following a regex literal should not be elided. + +v2.1.5 (2015-10-11) Tikitu de Jager +----------------------------------- + +- Fix #9: Premature end of statement caused by multi-line comment not + adding newline. + +- Fix #10: Removing multiline comment separating tokens must leave a space. + +- Refactor comment handling for maintainability. + +v2.1.4 (2015-08-23) Tikitu de Jager +----------------------------------- + +- Fix #6: regex literal matching comment was not correctly matched. + +- Refactor regex literal handling for robustness. + +v2.1.3 (2015-08-09) Tikitu de Jager +----------------------------------- + +- Reset issue numbering: issues live in github from now on. + +- Fix #1: regex literal was not recognised when occurring directly after `{`. + +v2.1.2 (2015-07-12) Tikitu de Jager +----------------------------------- + +- Issue numbers here and below refer to the bitbucket repository. + +- Fix #17: bug when JS starts with comment then literal regex. + +v2.1.1 (2015-02-14) Tikitu de Jager +----------------------------------- + +- Fix #16: bug returning a literal regex containing escaped forward-slashes. + +v2.1.0 (2014-12-24) Tikitu de Jager +----------------------------------- + +- First changelog entries; see README.rst for prior contributors. + +- Expose quote_chars parameter to provide just enough unofficial Harmony + support to be useful. + + + diff --git a/third_party/python/jsmin/jsmin.egg-info/SOURCES.txt b/third_party/python/jsmin/jsmin.egg-info/SOURCES.txt @@ -0,0 +1,14 @@ +CHANGELOG.txt +LICENSE.txt +MANIFEST.in +README.rst +notes.txt +setup.cfg +setup.py +jsmin/__init__.py +jsmin/__main__.py +jsmin/test.py +jsmin.egg-info/PKG-INFO +jsmin.egg-info/SOURCES.txt +jsmin.egg-info/dependency_links.txt +jsmin.egg-info/top_level.txt +\ No newline at end of file diff --git a/third_party/python/jsmin/jsmin.egg-info/dependency_links.txt b/third_party/python/jsmin/jsmin.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/third_party/python/jsmin/jsmin.egg-info/top_level.txt b/third_party/python/jsmin/jsmin.egg-info/top_level.txt @@ -0,0 +1 @@ +jsmin diff --git a/third_party/python/jsmin/jsmin/__init__.py b/third_party/python/jsmin/jsmin/__init__.py @@ -0,0 +1,252 @@ +# vim: set fileencoding=utf-8 : + +# This code is original from jsmin by Douglas Crockford, it was translated to +# Python by Baruch Even. It was rewritten by Dave St.Germain for speed. +# +# The MIT License (MIT) +# +# Copyright (c) 2013 Dave St.Germain +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + + +import io + +__all__ = ['jsmin', 'JavascriptMinify'] +__version__ = '3.0.1' + + +def jsmin(js, **kwargs): + """ + returns a minified version of the javascript string + """ + klass = io.StringIO + ins = klass(js) + outs = klass() + JavascriptMinify(ins, outs, **kwargs).minify() + return outs.getvalue() + + +class JavascriptMinify(object): + """ + Minify an input stream of javascript, writing + to an output stream + """ + + def __init__(self, instream=None, outstream=None, quote_chars="'\""): + self.ins = instream + self.outs = outstream + self.quote_chars = quote_chars + + def minify(self, instream=None, outstream=None): + if instream and outstream: + self.ins, self.outs = instream, outstream + + self.is_return = False + self.return_buf = '' + + def write(char): + # all of this is to support literal regular expressions. + # sigh + if char in 'return': + self.return_buf += char + self.is_return = self.return_buf == 'return' + else: + self.return_buf = '' + self.is_return = self.is_return and char < '!' + self.outs.write(char) + if self.is_return: + self.return_buf = '' + + read = self.ins.read + + space_strings = "abcdefghijklmnopqrstuvwxyz"\ + "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_$\\" + self.space_strings = space_strings + starters, enders = '{[(+-', '}])+-/' + self.quote_chars + newlinestart_strings = starters + space_strings + self.quote_chars + newlineend_strings = enders + space_strings + self.quote_chars + self.newlinestart_strings = newlinestart_strings + self.newlineend_strings = newlineend_strings + + do_newline = False + do_space = False + escape_slash_count = 0 + in_quote = '' + quote_buf = [] + + previous = ';' + previous_non_space = ';' + next1 = read(1) + + while next1: + next2 = read(1) + if in_quote: + quote_buf.append(next1) + + if next1 == in_quote: + numslashes = 0 + for c in reversed(quote_buf[:-1]): + if c != '\\': + break + else: + numslashes += 1 + if numslashes % 2 == 0: + in_quote = '' + write(''.join(quote_buf)) + elif next1 in '\r\n': + next2, do_newline = self.newline( + previous_non_space, next2, do_newline) + elif next1 < '!': + if (previous_non_space in space_strings \ + or previous_non_space > '~') \ + and (next2 in space_strings or next2 > '~'): + do_space = True + elif previous_non_space in '-+' and next2 == previous_non_space: + # protect against + ++ or - -- sequences + do_space = True + elif self.is_return and next2 == '/': + # returning a regex... + write(' ') + elif next1 == '/': + if do_space: + write(' ') + if next2 == '/': + # Line comment: treat it as a newline, but skip it + next2 = self.line_comment(next1, next2) + next1 = '\n' + next2, do_newline = self.newline( + previous_non_space, next2, do_newline) + elif next2 == '*': + self.block_comment(next1, next2) + next2 = read(1) + if previous_non_space in space_strings: + do_space = True + next1 = previous + else: + if previous_non_space in '{(,=:[?!&|;' or self.is_return: + self.regex_literal(next1, next2) + # hackish: after regex literal next1 is still / + # (it was the initial /, now it's the last /) + next2 = read(1) + else: + write('/') + else: + if do_newline: + write('\n') + do_newline = False + do_space = False + if do_space: + do_space = False + write(' ') + + write(next1) + if next1 in self.quote_chars: + in_quote = next1 + quote_buf = [] + + if next1 >= '!': + previous_non_space = next1 + + if next1 == '\\': + escape_slash_count += 1 + else: + escape_slash_count = 0 + + previous = next1 + next1 = next2 + + def regex_literal(self, next1, next2): + assert next1 == '/' # otherwise we should not be called! + + self.return_buf = '' + + read = self.ins.read + write = self.outs.write + + in_char_class = False + + write('/') + + next = next2 + while next and (next != '/' or in_char_class): + write(next) + if next == '\\': + write(read(1)) # whatever is next is escaped + elif next == '[': + write(read(1)) # character class cannot be empty + in_char_class = True + elif next == ']': + in_char_class = False + next = read(1) + + write('/') + + def line_comment(self, next1, next2): + assert next1 == next2 == '/' + + read = self.ins.read + + while next1 and next1 not in '\r\n': + next1 = read(1) + while next1 and next1 in '\r\n': + next1 = read(1) + + return next1 + + def block_comment(self, next1, next2): + assert next1 == '/' + assert next2 == '*' + + read = self.ins.read + + # Skip past first /* and avoid catching on /*/...*/ + next1 = read(1) + next2 = read(1) + + comment_buffer = '/*' + while next1 != '*' or next2 != '/': + comment_buffer += next1 + next1 = next2 + next2 = read(1) + + if comment_buffer.startswith("/*!"): + # comment needs preserving + self.outs.write(comment_buffer) + self.outs.write("*/\n") + + + def newline(self, previous_non_space, next2, do_newline): + read = self.ins.read + + if previous_non_space and ( + previous_non_space in self.newlineend_strings + or previous_non_space > '~'): + while 1: + if next2 < '!': + next2 = read(1) + if not next2: + break + else: + if next2 in self.newlinestart_strings \ + or next2 > '~' or next2 == '/': + do_newline = True + break + + return next2, do_newline diff --git a/third_party/python/jsmin/jsmin/__main__.py b/third_party/python/jsmin/jsmin/__main__.py @@ -0,0 +1,37 @@ +# vim: set fileencoding=utf-8 : + +# This code is original from jsmin by Douglas Crockford, it was translated to +# Python by Baruch Even. It was rewritten by Dave St.Germain for speed. +# +# The MIT License (MIT) +#· +# Copyright (c) 2013 Dave St.Germain +#· +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +#· +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +#· +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import sys, os, glob +from jsmin import JavascriptMinify + +for f in sys.argv[1:]: + with open(f, 'r') as js: + minifier = JavascriptMinify(js, sys.stdout) + minifier.minify() + sys.stdout.write('\n') + + diff --git a/third_party/python/jsmin/jsmin/test.py b/third_party/python/jsmin/jsmin/test.py @@ -0,0 +1,636 @@ +# vim: set fileencoding=utf-8 : + +# This code is original from jsmin by Douglas Crockford, it was translated to +# Python by Baruch Even. It was rewritten by Dave St.Germain for speed. +# +# The MIT License (MIT) +#· +# Copyright (c) 2013 Dave St.Germain +#· +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +#· +# The above copyright notice and this permission notice shall be included in +# all copies or substantial portions of the Software. +#· +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +# THE SOFTWARE. + +import unittest +import jsmin + + +class JsTests(unittest.TestCase): + def _minify(self, js): + return jsmin.jsmin(js) + + def assertEqual(self, thing1, thing2): + if thing1 != thing2: + print(repr(thing1), repr(thing2)) + raise AssertionError + return True + + def assertMinified(self, js_input, expected, **kwargs): + minified = jsmin.jsmin(js_input, **kwargs) + assert minified == expected, "\ngot: %r\nexp: %r" % (minified, expected) + + def testQuoted(self): + js = r''' + Object.extend(String, { + interpret: function(value) { + return value == null ? '' : String(value); + }, + specialChar: { + '\b': '\\b', + '\t': '\\t', + '\n': '\\n', + '\f': '\\f', + '\r': '\\r', + '\\': '\\\\' + } + }); + + ''' + expected = r"""Object.extend(String,{interpret:function(value){return value==null?'':String(value);},specialChar:{'\b':'\\b','\t':'\\t','\n':'\\n','\f':'\\f','\r':'\\r','\\':'\\\\'}});""" + self.assertMinified(js, expected) + + def testSingleComment(self): + js = r'''// use native browser JS 1.6 implementation if available + if (Object.isFunction(Array.prototype.forEach)) + Array.prototype._each = Array.prototype.forEach; + + if (!Array.prototype.indexOf) Array.prototype.indexOf = function(item, i) { + + // hey there + function() {// testing comment + foo; + //something something + + location = 'http://foo.com;'; // goodbye + } + //bye + ''' + expected = r"""if(Object.isFunction(Array.prototype.forEach)) +Array.prototype._each=Array.prototype.forEach;if(!Array.prototype.indexOf)Array.prototype.indexOf=function(item,i){function(){foo;location='http://foo.com;';}""" + self.assertMinified(js, expected) + + def testEmpty(self): + self.assertMinified('', '') + self.assertMinified(' ', '') + self.assertMinified('\n', '') + self.assertMinified('\r\n', '') + self.assertMinified('\t', '') + + + def testMultiComment(self): + js = r""" + function foo() { + print('hey'); + } + /* + if(this.options.zindex) { + this.originalZ = parseInt(Element.getStyle(this.element,'z-index') || 0); + this.element.style.zIndex = this.options.zindex; + } + */ + another thing; + """ + expected = r"""function foo(){print('hey');} +another thing;""" + self.assertMinified(js, expected) + + def testLeadingComment(self): + js = r"""/* here is a comment at the top + + it ends here */ + function foo() { + alert('crud'); + } + + """ + expected = r"""function foo(){alert('crud');}""" + self.assertMinified(js, expected) + + def testBlockCommentStartingWithSlash(self): + self.assertMinified('A; /*/ comment */ B', 'A;B') + + def testBlockCommentEndingWithSlash(self): + self.assertMinified('A; /* comment /*/ B', 'A;B') + + def testLeadingBlockCommentStartingWithSlash(self): + self.assertMinified('/*/ comment */ A', 'A') + + def testLeadingBlockCommentEndingWithSlash(self): + self.assertMinified('/* comment /*/ A', 'A') + + def testEmptyBlockComment(self): + self.assertMinified('/**/ A', 'A') + + def testBlockCommentMultipleOpen(self): + self.assertMinified('/* A /* B */ C', 'C') + + def testJustAComment(self): + self.assertMinified(' // a comment', '') + + def test_issue_bitbucket_10(self): + js = ''' + files = [{name: value.replace(/^.*\\\\/, '')}]; + // comment + A + ''' + expected = '''files=[{name:value.replace(/^.*\\\\/,'')}];A''' + self.assertMinified(js, expected) + + def test_issue_bitbucket_10_without_semicolon(self): + js = ''' + files = [{name: value.replace(/^.*\\\\/, '')}] + // comment + A + ''' + expected = '''files=[{name:value.replace(/^.*\\\\/,'')}]\nA''' + self.assertMinified(js, expected) + + def testRe(self): + js = r''' + var str = this.replace(/\\./g, '@').replace(/"[^"\\\n\r]*"/g, ''); + return (/^[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]*$/).test(str); + });''' + expected = r"""var str=this.replace(/\\./g,'@').replace(/"[^"\\\n\r]*"/g,'');return(/^[,:{}\[\]0-9.\-+Eaeflnr-u \n\r\t]*$/).test(str);});""" + self.assertMinified(js, expected) + + def testIgnoreComment(self): + js = r""" + var options_for_droppable = { + overlap: options.overlap, + containment: options.containment, + tree: options.tree, + hoverclass: options.hoverclass, + onHover: Sortable.onHover + } + + var options_for_tree = { + onHover: Sortable.onEmptyHover, + overlap: options.overlap, + containment: options.containment, + hoverclass: options.hoverclass + } + + // fix for gecko engine + Element.cleanWhitespace(element); + """ + expected = r"""var options_for_droppable={overlap:options.overlap,containment:options.containment,tree:options.tree,hoverclass:options.hoverclass,onHover:Sortable.onHover} +var options_for_tree={onHover:Sortable.onEmptyHover,overlap:options.overlap,containment:options.containment,hoverclass:options.hoverclass} +Element.cleanWhitespace(element);""" + self.assertMinified(js, expected) + + def testHairyRe(self): + js = r""" + inspect: function(useDoubleQuotes) { + var escapedString = this.gsub(/[\x00-\x1f\\]/, function(match) { + var character = String.specialChar[match[0]]; + return character ? character : '\\u00' + match[0].charCodeAt().toPaddedString(2, 16); + }); + if (useDoubleQuotes) return '"' + escapedString.replace(/"/g, '\\"') + '"'; + return "'" + escapedString.replace(/'/g, '\\\'') + "'"; + }, + + toJSON: function() { + return this.inspect(true); + }, + + unfilterJSON: function(filter) { + return this.sub(filter || Prototype.JSONFilter, '#{1}'); + }, + """ + expected = r"""inspect:function(useDoubleQuotes){var escapedString=this.gsub(/[\x00-\x1f\\]/,function(match){var character=String.specialChar[match[0]];return character?character:'\\u00'+match[0].charCodeAt().toPaddedString(2,16);});if(useDoubleQuotes)return'"'+escapedString.replace(/"/g,'\\"')+'"';return"'"+escapedString.replace(/'/g,'\\\'')+"'";},toJSON:function(){return this.inspect(true);},unfilterJSON:function(filter){return this.sub(filter||Prototype.JSONFilter,'#{1}');},""" + self.assertMinified(js, expected) + + def testLiteralRe(self): + js = r""" + myString.replace(/\\/g, '/'); + console.log("hi"); + """ + expected = r"""myString.replace(/\\/g,'/');console.log("hi");""" + self.assertMinified(js, expected) + + js = r''' return /^data:image\//i.test(url) || + /^(https?|ftp|file|about|chrome|resource):/.test(url); + ''' + expected = r'''return /^data:image\//i.test(url)||/^(https?|ftp|file|about|chrome|resource):/.test(url);''' + self.assertMinified(js, expected) + + def testNoBracesWithComment(self): + js = r""" + onSuccess: function(transport) { + var js = transport.responseText.strip(); + if (!/^\[.*\]$/.test(js)) // TODO: improve sanity check + throw 'Server returned an invalid collection representation.'; + this._collection = eval(js); + this.checkForExternalText(); + }.bind(this), + onFailure: this.onFailure + }); + """ + expected = r"""onSuccess:function(transport){var js=transport.responseText.strip();if(!/^\[.*\]$/.test(js)) +throw'Server returned an invalid collection representation.';this._collection=eval(js);this.checkForExternalText();}.bind(this),onFailure:this.onFailure});""" + self.assertMinified(js, expected) + js_without_comment = r""" + onSuccess: function(transport) { + var js = transport.responseText.strip(); + if (!/^\[.*\]$/.test(js)) + throw 'Server returned an invalid collection representation.'; + this._collection = eval(js); + this.checkForExternalText(); + }.bind(this), + onFailure: this.onFailure + }); + """ + self.assertMinified(js_without_comment, expected) + + def testSpaceInRe(self): + js = r""" + num = num.replace(/ /g,''); + """ + self.assertMinified(js, "num=num.replace(/ /g,'');") + + def testEmptyString(self): + js = r''' + function foo('') { + + } + ''' + self.assertMinified(js, "function foo(''){}") + + def testDoubleSpace(self): + js = r''' +var foo = "hey"; + ''' + self.assertMinified(js, 'var foo="hey";') + + def testLeadingRegex(self): + js = r'/[d]+/g ' + self.assertMinified(js, js.strip()) + + def testLeadingString(self): + js = r"'a string in the middle of nowhere'; // and a comment" + self.assertMinified(js, "'a string in the middle of nowhere';") + + def testSingleCommentEnd(self): + js = r'// a comment\n' + self.assertMinified(js, '') + + def testInputStream(self): + try: + from StringIO import StringIO + except ImportError: + from io import StringIO + + ins = StringIO(r''' + function foo('') { + + } + ''') + outs = StringIO() + m = jsmin.JavascriptMinify() + m.minify(ins, outs) + output = outs.getvalue() + assert output == "function foo(''){}" + + def testUnicode(self): + instr = u'\u4000 //foo' + expected = u'\u4000' + output = jsmin.jsmin(instr) + self.assertEqual(output, expected) + + def testCommentBeforeEOF(self): + self.assertMinified("//test\r\n", "") + + def testCommentInObj(self): + self.assertMinified("""{ + a: 1,//comment + }""", "{a:1,}") + + def testCommentInObj2(self): + self.assertMinified("{a: 1//comment\r\n}", "{a:1}") + + def testImplicitSemicolon(self): + # return \n 1 is equivalent with return; 1 + # so best make sure jsmin retains the newline + self.assertMinified("return\na", "return\na") + + def test_explicit_semicolon(self): + self.assertMinified("return;//comment\r\na", "return;a") + + def testImplicitSemicolon2(self): + self.assertMinified("return//comment...\r\nar", "return\nar") + + def testImplicitSemicolon3(self): + self.assertMinified("return//comment...\r\na", "return\na") + + def testSingleComment2(self): + self.assertMinified('x.replace(/\//, "_")// slash to underscore', + 'x.replace(/\//,"_")') + + def testSlashesNearComments(self): + original = ''' + { a: n / 2, } + // comment + ''' + expected = '''{a:n/2,}''' + self.assertMinified(original, expected) + + def testReturn(self): + original = ''' + return foo;//comment + return bar;''' + expected = 'return foo;return bar;' + self.assertMinified(original, expected) + original = ''' + return foo + return bar;''' + expected = 'return foo\nreturn bar;' + self.assertMinified(original, expected) + + def test_space_plus(self): + original = '"s" + ++e + "s"' + expected = '"s"+ ++e+"s"' + self.assertMinified(original, expected) + + def test_no_final_newline(self): + original = '"s"' + expected = '"s"' + self.assertMinified(original, expected) + + def test_space_with_regex_repeats(self): + original = '/(NaN| {2}|^$)/.test(a)&&(a="M 0 0");' + self.assertMinified(original, original) # there should be nothing jsmin can do here + + def test_space_with_regex_repeats_not_at_start(self): + original = 'aaa;/(NaN| {2}|^$)/.test(a)&&(a="M 0 0");' + self.assertMinified(original, original) # there should be nothing jsmin can do here + + def test_space_in_regex(self): + original = '/a (a)/.test("a")' + self.assertMinified(original, original) + + def test_brackets_around_slashed_regex(self): + original = 'function a() { /\//.test("a") }' + expected = 'function a(){/\//.test("a")}' + self.assertMinified(original, expected) + + def test_angular_1(self): + original = '''var /** holds major version number for IE or NaN for real browsers */ + msie, + jqLite, // delay binding since jQuery could be loaded after us.''' + minified = jsmin.jsmin(original) + self.assertTrue('var\nmsie' in minified) + + def test_angular_2(self): + original = 'var/* comment */msie;' + expected = 'var msie;' + self.assertMinified(original, expected) + + def test_angular_3(self): + original = 'var /* comment */msie;' + expected = 'var msie;' + self.assertMinified(original, expected) + + def test_angular_4(self): + original = 'var /* comment */ msie;' + expected = 'var msie;' + self.assertMinified(original, expected) + + def test_angular_5(self): + original = 'a/b' + self.assertMinified(original, original) + + def testBackticks(self): + original = '`test`' + self.assertMinified(original, original, quote_chars="'\"`") + + original = '` test with leading whitespace`' + self.assertMinified(original, original, quote_chars="'\"`") + + original = '`test with trailing whitespace `' + self.assertMinified(original, original, quote_chars="'\"`") + + original = '''`test +with a new line`''' + self.assertMinified(original, original, quote_chars="'\"`") + + original = '''dumpAvStats: function(stats) { + var statsString = ""; + if (stats.mozAvSyncDelay) { + statsString += `A/V sync: ${stats.mozAvSyncDelay} ms `; + } + if (stats.mozJitterBufferDelay) { + statsString += `Jitter-buffer delay: ${stats.mozJitterBufferDelay} ms`; + } + + return React.DOM.div(null, statsString);''' + expected = 'dumpAvStats:function(stats){var statsString="";if(stats.mozAvSyncDelay){statsString+=`A/V sync: ${stats.mozAvSyncDelay} ms `;}\nif(stats.mozJitterBufferDelay){statsString+=`Jitter-buffer delay: ${stats.mozJitterBufferDelay} ms`;}\nreturn React.DOM.div(null,statsString);' + self.assertMinified(original, expected, quote_chars="'\"`") + + def testBackticksExpressions(self): + original = '`Fifteen is ${a + b} and not ${2 * a + b}.`' + self.assertMinified(original, original, quote_chars="'\"`") + + original = '''`Fifteen is ${a + +b} and not ${2 * a + "b"}.`''' + self.assertMinified(original, original, quote_chars="'\"`") + + def testBackticksTagged(self): + original = 'tag`Hello ${ a + b } world ${ a * b}`;' + self.assertMinified(original, original, quote_chars="'\"`") + + def test_issue_bitbucket_16(self): + original = """ + f = function() { + return /DataTree\/(.*)\//.exec(this._url)[1]; + } + """ + self.assertMinified( + original, + 'f=function(){return /DataTree\/(.*)\//.exec(this._url)[1];}') + + def test_issue_bitbucket_17(self): + original = "// hi\n/^(get|post|head|put)$/i.test('POST')" + self.assertMinified(original, + "/^(get|post|head|put)$/i.test('POST')") + + def test_issue_6(self): + original = ''' + respond.regex = { + comments: /\/\*[^*]*\*+([^/][^*]*\*+)*\//gi, + urls: 'whatever' + }; + ''' + expected = original.replace(' ', '').replace('\n', '') + self.assertMinified(original, expected) + + def test_issue_9(self): + original = '\n'.join([ + 'var a = \'hi\' // this is a comment', + 'var a = \'hi\' /* this is also a comment */', + 'console.log(1) // this is a comment', + 'console.log(1) /* this is also a comment */', + '1 // this is a comment', + '1 /* this is also a comment */', + '{} // this is a comment', + '{} /* this is also a comment */', + '"YOLO" /* this is a comment */', + '"YOLO" // this is a comment', + '(1 + 2) // comment', + '(1 + 2) /* yup still comment */', + 'var b' + ]) + expected = '\n'.join([ + 'var a=\'hi\'', + 'var a=\'hi\'', + 'console.log(1)', + 'console.log(1)', + '1', + '1', + '{}', + '{}', + '"YOLO"', + '"YOLO"', + '(1+2)', + '(1+2)', + 'var b' + ]) + self.assertMinified(expected, expected) + self.assertMinified(original, expected) + + def test_newline_between_strings(self): + self.assertMinified('"yolo"\n"loyo"', '"yolo"\n"loyo"') + + def test_issue_10_comments_between_tokens(self): + self.assertMinified('var/* comment */a', 'var a') + + def test_ends_with_string(self): + self.assertMinified('var s = "s"', 'var s="s"') + + def test_short_comment(self): + self.assertMinified('a;/**/b', 'a;b') + + def test_shorter_comment(self): + self.assertMinified('a;/*/*/b', 'a;b') + + def test_block_comment_with_semicolon(self): + self.assertMinified('a;/**/\nb', 'a;b') + + def test_block_comment_With_implicit_semicolon(self): + self.assertMinified('a/**/\nvar b', 'a\nvar b') + + def test_issue_9_single_comments(self): + original = ''' + var a = "hello" // this is a comment + a += " world" + ''' + self.assertMinified(original, 'var a="hello"\na+=" world"') + + def test_issue_9_multi_comments(self): + original = ''' + var a = "hello" /* this is a comment */ + a += " world" + ''' + self.assertMinified(original, 'var a="hello"\na+=" world"') + + def test_issue_12_re_nl_if(self): + original = ''' + var re = /\d{4}/ + if (1) { console.log(2); }''' + self.assertMinified( + original, 'var re=/\d{4}/\nif(1){console.log(2);}') + + def test_issue_12_re_nl_other(self): + original = ''' + var re = /\d{4}/ + g = 10''' + self.assertMinified(original , 'var re=/\d{4}/\ng=10') + + def test_preserve_copyright(self): + original = ''' + function this() { + /*! Copyright year person */ + console.log('hello!'); + } + + /*! Copyright blah blah + * + * Some other text + */ + + var a; + ''' + expected = """function this(){/*! Copyright year person */ +console.log('hello!');}/*! Copyright blah blah + * + * Some other text + */\n\nvar a;""" + self.assertMinified(original, expected) + + def test_issue_14(self): + original = 'return x / 1;' + self.assertMinified(original, 'return x/1;') + + def test_issue_14_with_char_from_return(self): + original = 'return r / 1;' + self.assertMinified(original, 'return r/1;') + + +class RegexTests(unittest.TestCase): + + def regex_recognise(self, js): + klass = jsmin.io.StringIO + ins = klass(js[2:]) + outs = klass() + jsmin.JavascriptMinify(ins, outs).regex_literal(js[0], js[1]) + return outs.getvalue() + + def assert_regex(self, js_input, expected): + assert js_input[0] == '/' # otherwise we should not be testing! + recognised = self.regex_recognise(js_input) + assert recognised == expected, "\n in: %r\ngot: %r\nexp: %r" % (js_input, recognised, expected) + + def test_simple(self): + self.assert_regex('/123/g', '/123/') + + def test_character_class(self): + self.assert_regex('/a[0-9]b/g', '/a[0-9]b/') + + def test_character_class_with_slash(self): + self.assert_regex('/a[/]b/g', '/a[/]b/') + + def test_escaped_forward_slash(self): + self.assert_regex(r'/a\/b/g', r'/a\/b/') + + def test_escaped_back_slash(self): + self.assert_regex(r'/a\\/g', r'/a\\/') + + def test_empty_character_class(self): + # This one is subtle: an empty character class is not allowed, afaics + # from http://regexpal.com/ Chrome Version 44.0.2403.155 (64-bit) Mac + # so this char class is interpreted as containing ]/ *not* as char + # class [] followed by end-of-regex /. + self.assert_regex('/a[]/]b/g', '/a[]/]b/') + + def test_precedence_of_parens(self): + # judging from + # http://regexpal.com/ Chrome Version 44.0.2403.155 (64-bit) Mac + # () have lower precedence than [] + self.assert_regex('/a([)])b/g', '/a([)])b/') + self.assert_regex('/a[(]b/g', '/a[(]b/') + +if __name__ == '__main__': + unittest.main() diff --git a/third_party/python/jsmin/notes.txt b/third_party/python/jsmin/notes.txt @@ -0,0 +1,3 @@ +python3 -m venv . +source bin/activate +pip install twine diff --git a/third_party/python/jsmin/setup.cfg b/third_party/python/jsmin/setup.cfg @@ -0,0 +1,5 @@ +[egg_info] +tag_build = +tag_date = 0 +tag_svn_revision = 0 + diff --git a/third_party/python/jsmin/setup.py b/third_party/python/jsmin/setup.py @@ -0,0 +1,36 @@ +from setuptools import setup + +import os, sys, re + +os.environ['COPYFILE_DISABLE'] = 'true' # this disables including resource forks in tar files on os x + + +def long_description(): + return open('README.rst').read() + '\n' + open('CHANGELOG.txt').read() + + +setup( + name="jsmin", + version=re.search(r'__version__ = ["\']([^"\']+)', open('jsmin/__init__.py').read()).group(1), + packages=['jsmin'], + description='JavaScript minifier.', + long_description=long_description(), + author='Dave St.Germain', + author_email='dave@st.germa.in', + maintainer='Tikitu de Jager', + maintainer_email='tikitu+jsmin@logophile.org', + test_suite='jsmin.test', + license='MIT License', + url='https://github.com/tikitu/jsmin/', + classifiers=[ + 'Development Status :: 5 - Production/Stable', + 'Environment :: Web Environment', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: MIT License', + 'Operating System :: OS Independent', + 'Programming Language :: Python :: 3 :: Only', + 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', + 'Topic :: Software Development :: Pre-processors', + 'Topic :: Text Processing :: Filters', + ] +) diff --git a/third_party/python/pyproject.toml b/third_party/python/pyproject.toml @@ -25,6 +25,7 @@ dependencies = [ "importlib-metadata~=8.0", # Required for compatibility with Flask >= 2 in tools/tryselect/selectors/chooser "jinja2==3.1.6", + "jsmin~=3.0", "json-e~=4.5", "jsonschema==4.17.3", "looseversion~=1.0", diff --git a/third_party/python/requirements.txt b/third_party/python/requirements.txt @@ -465,6 +465,9 @@ jinxed==1.3.0 \ --hash=sha256:1593124b18a41b7a3da3b078471442e51dbad3d77b4d4f2b0c26ab6f7d660dbf \ --hash=sha256:b993189f39dc2d7504d802152671535b06d380b26d78070559551cbf92df4fc5 # via blessed +jsmin==3.0.1 \ + --hash=sha256:c0959a121ef94542e807a674142606f7e90214a2b3d1eb17300244bbb5cc2bfc + # via mozilla-third-party-python-vendor-dir json-e==4.8.0 \ --hash=sha256:51ead93962912d701c6f1a6a0b27cc34bb2cb8397a82affd8adb2401898e27ea \ --hash=sha256:91a50ba4e1a9e6d40c36c0601d68acda9ae44ca2817525e09938b2c82ce23572 diff --git a/third_party/python/uv.lock b/third_party/python/uv.lock @@ -689,6 +689,12 @@ wheels = [ ] [[package]] +name = "jsmin" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/73/e01e4c5e11ad0494f4407a3f623ad4d87714909f50b17a06ed121034ff6e/jsmin-3.0.1.tar.gz", hash = "sha256:c0959a121ef94542e807a674142606f7e90214a2b3d1eb17300244bbb5cc2bfc", size = 13925, upload-time = "2022-01-16T20:35:59.13Z" } + +[[package]] name = "json-e" version = "4.8.0" source = { registry = "https://pypi.org/simple" } @@ -896,6 +902,7 @@ dependencies = [ { name = "glean-parser" }, { name = "importlib-metadata" }, { name = "jinja2" }, + { name = "jsmin" }, { name = "json-e" }, { name = "jsonschema" }, { name = "looseversion" }, @@ -958,6 +965,7 @@ requires-dist = [ { name = "glean-parser", specifier = "==18.1.0" }, { name = "importlib-metadata", specifier = "~=8.0" }, { name = "jinja2", specifier = "==3.1.6" }, + { name = "jsmin", specifier = "~=3.0" }, { name = "json-e", specifier = "~=4.5" }, { name = "jsonschema", specifier = "==4.17.3" }, { name = "looseversion", specifier = "~=1.0" }, diff --git a/third_party/python/uv.lock.hash b/third_party/python/uv.lock.hash @@ -1 +1 @@ -a570584e193aedfae04b7555eb45c35bd994c7a23887b864027941f8b02776ad -\ No newline at end of file +573c187ac7fe7291e15fee884fe1d1889283b59e1e5c001fa534f9674b5024b0 +\ No newline at end of file diff --git a/toolkit/moz.configure b/toolkit/moz.configure @@ -1054,7 +1054,7 @@ set_config("MOZ_PACKAGER_FORMAT", packager_format) @depends(target_is_android, "--enable-debug", milestone.is_nightly) def enable_minify_default(is_android, debug, is_nightly): if is_android and not debug and not is_nightly: - return ("properties",) + return ("properties", "js") return ("properties",) @@ -1081,21 +1081,6 @@ set_config("MOZ_PACKAGER_MINIFY", True, when=enable_minify.properties) set_config("MOZ_PACKAGER_MINIFY_JS", True, when=enable_minify.js) -@depends(target_is_android, "--enable-debug", milestone.is_nightly) -def enable_minify_pdfjs_default(is_android, debug, is_nightly): - return is_android and not debug and not is_nightly - - -option( - name="--enable-minify-pdfjs", - help="Enable minification of PDF.js JavaScript files during packaging", - default=enable_minify_pdfjs_default, -) - - -set_config("MOZ_PACKAGER_MINIFY_PDFJS", True, when="--enable-minify-pdfjs") - - @depends(host, build_project) def jar_maker_format(host, build_project): # Multilocales for mobile/android use the same mergedirs for all locales, diff --git a/toolkit/mozapps/installer/js-compare-ast.js b/toolkit/mozapps/installer/js-compare-ast.js @@ -0,0 +1,31 @@ +/* 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/. */ + +/** + * This script compares the AST of two JavaScript files passed as arguments. + * The script exits with a 0 status code if both files parse properly and the + * ASTs of both files are identical modulo location differences. The script + * exits with status code 1 if any of these conditions don't hold. + * + * This script is used as part of packaging to verify minified JavaScript files + * are identical to their original files. + */ + +// Available to the js shell. +/* global snarf, scriptArgs, quit */ + +"use strict"; + +function ast(filename) { + return JSON.stringify(Reflect.parse(snarf(filename), { loc: 0 })); +} + +if (scriptArgs.length !== 2) { + throw new Error("usage: js js-compare-ast.js FILE1.js FILE2.js"); +} + +var ast0 = ast(scriptArgs[0]); +var ast1 = ast(scriptArgs[1]); + +quit(ast0 == ast1 ? 0 : 1); diff --git a/toolkit/mozapps/installer/packager.mk b/toolkit/mozapps/installer/packager.mk @@ -32,7 +32,6 @@ stage-package: multilocale.txt locale-manifest.in $(MOZ_PKG_MANIFEST) $(MOZ_PKG_ $(if $(MOZ_PACKAGER_MINIFY_JS),--minify-js \ $(addprefix --js-binary ,$(JS_BINARY)) \ ) \ - $(if $(MOZ_PACKAGER_MINIFY_PDFJS),--minify-pdfjs) \ $(addprefix --jarlog ,$(wildcard $(JARLOG_FILE_AB_CD))) \ $(addprefix --compress ,$(JAR_COMPRESSION)) \ $(MOZ_PKG_MANIFEST) '$(DIST)' '$(DIST)'/$(MOZ_PKG_DIR)$(if $(MOZ_PKG_MANIFEST),,$(_BINPATH:%=/%)) \ diff --git a/toolkit/mozapps/installer/packager.py b/toolkit/mozapps/installer/packager.py @@ -153,11 +153,6 @@ def main(): help="Minify JavaScript files while packaging.", ) parser.add_argument( - "--minify-pdfjs", - action="store_true", - help="Minify PDF.js JavaScript files while packaging.", - ) - parser.add_argument( "--js-binary", help="Path to js binary. This is used to verify " "minified JavaScript. If this is not defined, " @@ -228,9 +223,15 @@ def main(): finder_args = dict( minify=args.minify, minify_js=args.minify_js, - minify_pdfjs=args.minify_pdfjs, ignore_broken_symlinks=args.ignore_broken_symlinks, ) + if args.js_binary: + finder_args["minify_js_verify_command"] = [ + args.js_binary, + os.path.join( + os.path.abspath(os.path.dirname(__file__)), "js-compare-ast.js" + ), + ] finder = PackagerFileFinder(args.source, find_executables=True, **finder_args) if "NO_PKG_FILES" in os.environ: sinkformatter = NoPkgFilesRemover(formatter, args.manifest is not None) diff --git a/tools/browsertime/mach_commands.py b/tools/browsertime/mach_commands.py @@ -280,7 +280,8 @@ def setup_browsertime( ): r"""Install browsertime and visualmetrics.py prerequisites and the Node.js package.""" - from mozbuild.nodeutil import check_node_executables_valid, package_setup + sys.path.append(mozpath.join(command_context.topsrcdir, "tools", "lint", "eslint")) + import setup_helper if not new_upstream_url: setup_prerequisites(command_context) @@ -318,7 +319,7 @@ def setup_browsertime( f.write(updated_body) # Install the browsertime Node.js requirements. - if not check_node_executables_valid(): + if not setup_helper.check_node_executables_valid(): return 1 # To use a custom `geckodriver`, set @@ -352,7 +353,7 @@ def setup_browsertime( if IS_APPLE_SILICON and node_dir not in os.environ["PATH"]: os.environ["PATH"] += os.pathsep + node_dir - status = package_setup( + status = setup_helper.package_setup( BROWSERTIME_ROOT, "browsertime", should_update=new_upstream_url != "", @@ -580,10 +581,11 @@ def extra_default_args(command_context, args=[]): def _verify_node_install(command_context): # check if Node is installed - from mozbuild.nodeutil import check_node_executables_valid + sys.path.append(mozpath.join(command_context.topsrcdir, "tools", "lint", "eslint")) + import setup_helper with silence(): - node_valid = check_node_executables_valid() + node_valid = setup_helper.check_node_executables_valid() if not node_valid: print("Can't find Node. did you run ./mach bootstrap ?") return False diff --git a/tools/lint/eslint/__init__.py b/tools/lint/eslint/__init__.py @@ -11,7 +11,7 @@ import subprocess import sys sys.path.append(os.path.join(os.path.dirname(__file__), "eslint")) -from mozbuild.nodeutil import check_node_executables_valid, find_node_executable +from mozbuild.nodeutil import find_node_executable from mozlint import result from eslint import prettier_utils, setup_helper @@ -36,7 +36,7 @@ and try again. def setup(root, **lintargs): setup_helper.set_project_root(root) - if not check_node_executables_valid(): + if not setup_helper.check_node_executables_valid(): return 1 return setup_helper.eslint_maybe_setup() diff --git a/tools/lint/eslint/setup_helper.py b/tools/lint/eslint/setup_helper.py @@ -6,15 +6,51 @@ import json import os +import platform import re +import subprocess +import sys from filecmp import dircmp from mozbuild.nodeutil import ( - package_setup, - remove_directory, + NODE_MIN_VERSION, + NPM_MIN_VERSION, + find_node_executable, + find_npm_executable, ) +from mozfile.mozfile import remove as mozfileremove from packaging.version import Version +NODE_MACHING_VERSION_NOT_FOUND_MESSAGE = """ +Could not find Node.js executable later than %s. + +Executing `mach bootstrap --no-system-changes` should +install a compatible version in ~/.mozbuild on most platforms. +""".strip() + +NPM_MACHING_VERSION_NOT_FOUND_MESSAGE = """ +Could not find npm executable later than %s. + +Executing `mach bootstrap --no-system-changes` should +install a compatible version in ~/.mozbuild on most platforms. +""".strip() + +NODE_NOT_FOUND_MESSAGE = """ +nodejs is either not installed or is installed to a non-standard path. + +Executing `mach bootstrap --no-system-changes` should +install a compatible version in ~/.mozbuild on most platforms. +""".strip() + +NPM_NOT_FOUND_MESSAGE = """ +Node Package Manager (npm) is either not installed or installed to a +non-standard path. + +Executing `mach bootstrap --no-system-changes` should +install a compatible version in ~/.mozbuild on most platforms. +""".strip() + + VERSION_RE = re.compile(r"^\d+\.\d+\.\d+$") CARET_VERSION_RANGE_RE = re.compile(r"^\^((\d+)\.\d+\.\d+)$") @@ -49,12 +85,129 @@ def eslint_setup(package_root, package_name, should_clobber=False): os.path.join(get_eslint_module_path(), "eslint-plugin-mozilla", "node_modules") ) + package_setup(package_root, package_name, should_clobber=should_clobber) + + +def remove_directory(path, skip_logging=False): + if not skip_logging: + print("Clobbering %s..." % path) + if sys.platform.startswith("win") and have_winrm(): + process = subprocess.Popen(["winrm", "-rf", path]) + process.wait() + else: + mozfileremove(path) + + +def package_setup( + package_root, + package_name, + should_update=False, + should_clobber=False, + no_optional=False, + skip_logging=False, +): + """Ensure `package_name` at `package_root` is installed. + + When `should_update` is true, clobber, install, and produce a new + "package-lock.json" file. + + This populates `package_root/node_modules`. + + """ orig_project_root = get_project_root() + orig_cwd = os.getcwd() + + if should_update: + should_clobber = True + try: set_project_root(package_root) - package_setup(package_root, package_name, should_clobber=should_clobber) + sys.path.append(os.path.dirname(__file__)) + + # npm sometimes fails to respect cwd when it is run using check_call so + # we manually switch folders here instead. + project_root = get_project_root() + os.chdir(project_root) + + if should_clobber: + remove_directory(os.path.join(project_root, "node_modules"), skip_logging) + + npm_path, _ = find_npm_executable() + if not npm_path: + return 1 + + node_path, _ = find_node_executable() + if not node_path: + return 1 + + extra_parameters = ["--loglevel=error"] + + if no_optional: + extra_parameters.append("--no-optional") + + package_lock_json_path = os.path.join(get_project_root(), "package-lock.json") + + if should_update: + cmd = [npm_path, "install"] + mozfileremove(package_lock_json_path) + else: + cmd = [npm_path, "ci"] + + # On non-Windows, ensure npm is called via node, as node may not be in the + # path. + if platform.system() != "Windows": + cmd.insert(0, node_path) + + cmd.extend(extra_parameters) + + # Ensure that bare `node` and `npm` in scripts, including post-install scripts, finds the + # binary we're invoking with. Without this, it's easy for compiled extensions to get + # mismatched versions of the Node.js extension API. + path = os.environ.get("PATH", "").split(os.pathsep) + node_dir = os.path.dirname(node_path) + if node_dir not in path: + path = [node_dir] + path + + if not skip_logging: + print( + 'Installing %s for mach using "%s"...' % (package_name, " ".join(cmd)) + ) + result = call_process( + package_name, cmd, append_env={"PATH": os.pathsep.join(path)} + ) + + if not result: + return 1 + + bin_path = os.path.join( + get_project_root(), "node_modules", ".bin", package_name + ) + + if not skip_logging: + print("\n%s installed successfully!" % package_name) + print("\nNOTE: Your local %s binary is at %s\n" % (package_name, bin_path)) + finally: set_project_root(orig_project_root) + os.chdir(orig_cwd) + + +def call_process(name, cmd, cwd=None, append_env={}): + env = dict(os.environ) + env.update(append_env) + + try: + with open(os.devnull, "w") as fnull: + subprocess.check_call(cmd, cwd=cwd, stdout=fnull, env=env) + except subprocess.CalledProcessError: + if cwd: + print("\nError installing %s in the %s folder, aborting." % (name, cwd)) + else: + print("\nError installing %s, aborting." % name) + + return False + + return True def expected_installed_modules(package_root, package_name): @@ -185,6 +338,33 @@ def version_in_range(version, version_range): return False +def get_possible_node_paths_win(): + """ + Return possible nodejs paths on Windows. + """ + if platform.system() != "Windows": + return [] + + return list( + { + "%s\\nodejs" % os.environ.get("SystemDrive"), + os.path.join(os.environ.get("ProgramFiles"), "nodejs"), + os.path.join(os.environ.get("PROGRAMW6432"), "nodejs"), + os.path.join(os.environ.get("PROGRAMFILES"), "nodejs"), + } + ) + + +def get_version(path): + try: + version_str = subprocess.check_output( + [path, "--version"], stderr=subprocess.STDOUT, universal_newlines=True + ) + return version_str + except (subprocess.CalledProcessError, OSError): + return None + + def set_project_root(root=None): """Sets the project root to the supplied path, or works out what the root is based on looking for 'mach'. @@ -226,3 +406,34 @@ def get_project_root(): def get_eslint_module_path(): return os.path.join(get_project_root(), "tools", "lint", "eslint") + + +def check_node_executables_valid(): + node_path, version = find_node_executable() + if not node_path: + print(NODE_NOT_FOUND_MESSAGE) + return False + if not version: + print(NODE_MACHING_VERSION_NOT_FOUND_MESSAGE % NODE_MIN_VERSION) + return False + + npm_path, version = find_npm_executable() + if not npm_path: + print(NPM_NOT_FOUND_MESSAGE) + return False + if not version: + print(NPM_MACHING_VERSION_NOT_FOUND_MESSAGE % NPM_MIN_VERSION) + return False + + return True + + +def have_winrm(): + # `winrm -h` should print 'winrm version ...' and exit 1 + try: + p = subprocess.Popen( + ["winrm.exe", "-h"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT + ) + return p.wait() == 1 and p.stdout.read().startswith("winrm") + except Exception: + return False diff --git a/tools/lint/node-licenses.yml b/tools/lint/node-licenses.yml @@ -7,7 +7,6 @@ node-licenses: - package.json - tools/lint/eslint/eslint-plugin-mozilla/package.json - tools/lint/stylelint/stylelint-plugin-mozilla/package.json - - tools/terser/package.json exclude: [] extensions: - json diff --git a/tools/lint/node-licenses/__init__.py b/tools/lint/node-licenses/__init__.py @@ -10,7 +10,7 @@ import sys sys.path.append(os.path.join(os.path.dirname(__file__), "eslint")) from eslint import setup_helper -from mozbuild.nodeutil import check_node_executables_valid, find_node_executable +from mozbuild.nodeutil import find_node_executable from mozlint import result from mozlint.pathutils import expand_exclusions @@ -36,7 +36,7 @@ and try again. def setup(root, **lintargs): setup_helper.set_project_root(root) - if not check_node_executables_valid(): + if not setup_helper.check_node_executables_valid(): return 1 return setup_helper.eslint_maybe_setup() diff --git a/tools/lint/stylelint/__init__.py b/tools/lint/stylelint/__init__.py @@ -13,7 +13,7 @@ import sys sys.path.append(os.path.join(os.path.dirname(__file__), "eslint")) from eslint import prettier_utils, setup_helper -from mozbuild.nodeutil import check_node_executables_valid, find_node_executable +from mozbuild.nodeutil import find_node_executable from mozlint import result STYLELINT_ERROR_MESSAGE = """ @@ -38,7 +38,7 @@ FILE_EXT_REGEX = re.compile(r"\.[a-z0-9_]{2,10}$", re.IGNORECASE) def setup(root, **lintargs): setup_helper.set_project_root(root) - if not check_node_executables_valid(): + if not setup_helper.check_node_executables_valid(): return 1 return setup_helper.eslint_maybe_setup() diff --git a/tools/moztreedocs/mach_commands.py b/tools/moztreedocs/mach_commands.py @@ -123,11 +123,10 @@ def build_docs( ): # TODO: Bug 1704891 - move the ESLint setup tools to a shared place. import setup_helper - from mozbuild.nodeutil import check_node_executables_valid setup_helper.set_project_root(command_context.topsrcdir) - if not check_node_executables_valid(): + if not setup_helper.check_node_executables_valid(): return 1 setup_helper.eslint_maybe_setup() diff --git a/tools/terser/package-lock.json b/tools/terser/package-lock.json @@ -1,120 +0,0 @@ -{ - "name": "terser", - "version": "1.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "version": "1.0.0", - "dependencies": { - "terser": "5.44.0" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.11", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", - "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.30", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz", - "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "license": "MIT" - }, - "node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "license": "MIT" - }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/terser": { - "version": "5.44.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.0.tgz", - "integrity": "sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==", - "license": "BSD-2-Clause", - "dependencies": { - "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.15.0", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - }, - "bin": { - "terser": "bin/terser" - }, - "engines": { - "node": ">=10" - } - } - } -} diff --git a/tools/terser/package.json b/tools/terser/package.json @@ -1,8 +0,0 @@ -{ - "version": "1.0.0", - "description": "Terser JavaScript minifier for Mozilla builds", - "private": true, - "dependencies": { - "terser": "5.44.0" - } -} diff --git a/tools/ts/mach_commands.py b/tools/ts/mach_commands.py @@ -174,9 +174,8 @@ def node(ctx, script, *args): def maybe_setup(ctx): sys.path.append(mozpath.join(ctx.topsrcdir, "tools", "lint", "eslint")) import setup_helper - from mozbuild.nodeutil import check_node_executables_valid - if not check_node_executables_valid(): + if not setup_helper.check_node_executables_valid(): return 1 setup_helper.eslint_maybe_setup(package_name="TypeScript")