tor-browser

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

commit a44d6a597f7e99b3cf492807690fc539f5433895
parent 6173a356155d99f3f82de45f3e1c856642110ea2
Author: Alex Hochheiden <ahochheiden@mozilla.com>
Date:   Sat, 15 Nov 2025 03:20:54 +0000

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

- `PDF.js` is now always minified, regardless of config settings.

Differential Revision: https://phabricator.services.mozilla.com/D264160

Diffstat:
Mpython/mozbuild/mozpack/files.py | 182++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------------
Dpython/mozbuild/mozpack/test/support/minify_js_verify.py | 15---------------
Mpython/mozbuild/mozpack/test/test_files.py | 120++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------------------
Mtaskcluster/kinds/source-test/python-android.yml | 1+
Mtoolkit/moz.configure | 2+-
Dtoolkit/mozapps/installer/js-compare-ast.js | 31-------------------------------
Mtoolkit/mozapps/installer/packager.py | 7-------
7 files changed, 212 insertions(+), 146 deletions(-)

diff --git a/python/mozbuild/mozpack/files.py b/python/mozbuild/mozpack/files.py @@ -3,25 +3,26 @@ # 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, StringIO +from io import BytesIO from itertools import chain, takewhile +from pathlib import Path from tarfile import TarFile, TarInfo -from tempfile import NamedTemporaryFile, mkstemp - -from jsmin import JavascriptMinify +from tempfile import mkstemp 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 @@ -757,75 +758,134 @@ class MinifiedCommentStripped(BaseFile): class MinifiedJavaScript(BaseFile): """ - File class for minifying JavaScript files. + Minify JavaScript files using Terser while preserving + class and function names for better debugging. """ - def __init__(self, file, verify_command=None): - assert isinstance(file, BaseFile) + 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. + """ self._file = file - self._verify_command = verify_command + self._filepath = filepath - 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) + 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 self._verify_command: - return output + if not terser_path.exists(): + # Automatically set up node_modules if terser is not found + package_setup(str(terser_dir), "terser") - input_source = self._file.open().read() + # 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." + ) - with NamedTemporaryFile("wb+") as fh1, NamedTemporaryFile("wb+") as fh2: - fh1.write(input_source) - fh2.write(output_source) - fh1.flush() - fh2.flush() + terser_cmd = [node_path, str(terser_path)] + + with tempfile.TemporaryDirectory() as temp_dir: + temp_path = Path(temp_dir) + config_path = temp_path / "terser_config.json" + source_path = temp_path / "source.js" + + config_path.write_text(json.dumps(self.TERSER_CONFIG), encoding="utf-8") + source_path.write_bytes(source_content) try: - 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>")) + result = subprocess.run( + terser_cmd + + [ + source_path, + "--config-file", + config_path, + ], + capture_output=True, + check=False, ) - # 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) - return self._file.open() + 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 - return output + except subprocess.SubprocessError as e: + errors.error(f"Error running Terser for {self._filepath}: {e}") + return source_content + + 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) class BaseFinder: - def __init__( - self, base, minify=False, minify_js=False, minify_js_verify_command=None - ): + def __init__(self, base, minify=False, minify_js=False): """ 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_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.") @@ -833,7 +893,6 @@ class BaseFinder: self.base = mozpath.normsep(base) self._minify = minify self._minify_js = minify_js - self._minify_js_verify_command = minify_js_verify_command def find(self, pattern): """ @@ -897,8 +956,17 @@ class BaseFinder: if path.endswith((".ftl", ".properties")): return MinifiedCommentStripped(file) - if self._minify_js and path.endswith((".js", ".jsm", ".mjs")): - return MinifiedJavaScript(file, self._minify_js_verify_command) + 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 always get minified + if "pdfjs" in file_path: + return MinifiedJavaScript(file, path) + elif self._minify_js: + return MinifiedJavaScript(file, path) return file @@ -939,7 +1007,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 @@ -1,15 +0,0 @@ -# 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,9 +2,12 @@ # 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, errors +from mozpack.errors import ErrorMessage from mozpack.files import ( AbsoluteSymlinkFile, ComposedFinder, @@ -38,7 +41,7 @@ import random import sys import tarfile import unittest -from io import BytesIO, StringIO +from io import BytesIO from tempfile import mkdtemp import mozfile @@ -891,44 +894,91 @@ 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): - 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, - ] + """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; + } - 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")) + // Export for testing + var exported = testFunction; + """ - mini_lines = [s.decode() for s in min_f.open().readlines()] - self.assertTrue(mini_lines) - self.assertTrue(len(mini_lines) < len(self.orig_lines)) + orig_f = GeneratedFile(complex_js.encode()) + min_f = MinifiedJavaScript(orig_f, "complex.js") - 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")) + minified_content = min_f.open().read().decode() - 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()) + # 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"") + + 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'; }" + + orig_f = GeneratedFile(broken_js) + min_f = MinifiedJavaScript(orig_f, "broken.js") + + # Should raise an ErrorMessage when minification fails + from mozpack.errors import ErrorMessage + + with self.assertRaises(ErrorMessage): + min_f.open().read() class MatchTestTemplate: diff --git a/taskcluster/kinds/source-test/python-android.yml b/taskcluster/kinds/source-test/python-android.yml @@ -42,6 +42,7 @@ android-gradle-build: - linux64-android-sdk-linux-repack - linux64-jdk-repack - linux64-node + - terser when: files-changed: # Build stuff. 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", "js") + return ("properties",) return ("properties",) diff --git a/toolkit/mozapps/installer/js-compare-ast.js b/toolkit/mozapps/installer/js-compare-ast.js @@ -1,31 +0,0 @@ -/* 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.py b/toolkit/mozapps/installer/packager.py @@ -225,13 +225,6 @@ def main(): minify_js=args.minify_js, 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)