#!/usr/bin/env python3 import argparse import json import os import subprocess import sys from typing import NoReturn sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) os.chdir(os.path.join(os.path.dirname(__file__), "../web")) from version import ZULIP_VERSION def build_for_prod_or_puppeteer(quiet: bool, config_name: str | None = None) -> NoReturn: """Builds for production, writing the output to disk""" with open("/proc/meminfo") as meminfo: if int(next(meminfo).split()[1]) < 3 * 1024 * 1024: os.environ["NODE_OPTIONS"] = "--max-old-space-size=1536" webpack_args = [ "../node_modules/.bin/webpack-cli", "--mode=production", f"--env=ZULIP_VERSION={ZULIP_VERSION}", ] if quiet: webpack_args += ["--stats=errors-only"] if config_name is not None: webpack_args += [f"--config-name={config_name}"] if "PUPPETEER_TESTS" in os.environ: webpack_args.append("--env=puppeteer_tests") # Silence warnings from "browserslist" about using old data; those # warnings are only useful for development os.environ["BROWSERSLIST_IGNORE_OLD_DATA"] = "1" os.execvp(webpack_args[0], webpack_args) def build_for_dev_server(host: str, port: str, minify: bool, disable_host_check: bool) -> None: """watches and rebuilds on changes, serving files from memory via webpack-dev-server""" # This is our most dynamic configuration, which we use for our # dev server. The key piece here is that we identify changes to # files as devs make edits and serve new assets on the fly. webpack_args = ["../node_modules/.bin/webpack-cli", "serve"] webpack_args += [ # webpack-cli has a bug where it ignores --watch-poll with # multi-config, and we don't need the katex part anyway. "--config-name=frontend", f"--host={host}", f"--port={port}", # We add the hot flag using the cli because it takes care # of addition to entry points and adding the plugin # automatically "--hot", ] if minify: webpack_args.append("--env=minimize") if disable_host_check: webpack_args.append("--allowed-hosts=all") else: webpack_args.append(f"--allowed-hosts={host},.zulipdev.com,.zulipdev.org") # Tell webpack-dev-server to fall back to periodic polling on # filesystems where inotify is known to be broken. cwd = os.getcwd() inotify_broken = False with open("/proc/self/mounts") as mounts: for line in mounts: fields = line.split() if fields[1] == cwd: inotify_broken = fields[2] in ["nfs", "vboxsf", "prl_fs"] if inotify_broken: os.environ["CHOKIDAR_USEPOLLING"] = "1" os.environ["CHOKIDAR_INTERVAL"] = "1000" # TODO: This process should also fall back to periodic polling if # inotify_broken. import pyinotify try: webpack_process = subprocess.Popen(webpack_args) class WebpackConfigFileChangeHandler(pyinotify.ProcessEvent): def process_default(self, event: pyinotify.Event) -> None: nonlocal webpack_process print("Restarting webpack-dev-server due to config changes...") webpack_process.terminate() webpack_process.wait() webpack_process = subprocess.Popen(webpack_args) watch_manager = pyinotify.WatchManager() event_notifier = pyinotify.Notifier(watch_manager, WebpackConfigFileChangeHandler()) # We did a chdir to the root of the Zulip checkout above. for filepath in [ "webpack.config.ts", "webpack.assets.json", "webpack.dev-assets.json", ]: watch_manager.add_watch(filepath, pyinotify.IN_MODIFY) event_notifier.loop() finally: webpack_process.terminate() webpack_process.wait() def build_for_most_tests() -> None: """Generates a stub asset stat file for django so backend test can render a page""" # Tests like test-backend, test-api, and test-home-documentation use # our "test" configuration. The one exception is Puppeteer, which uses # our production configuration. # # It's not completely clear why we don't also use the same # configuration for ALL tests, but figuring out the full history here # was out of the scope of the effort here to add some comments and # clean up names. with open("webpack.assets.json") as json_data: entries = json.load(json_data) with open("webpack.dev-assets.json") as json_data: entries.update(json.load(json_data)) stat_data = { "status": "done", "chunks": {entry: [f"{entry}-stubentry.js"] for entry in entries}, } directory = "../var" if not os.path.exists(directory): os.makedirs(directory) with open(os.path.join(directory, "webpack-stats-test.json"), "w") as outfile: json.dump(stat_data, outfile) parser = argparse.ArgumentParser() parser.add_argument( "--test", action="store_true", help="generate a stub webpack-stats.json file (for backend testing)", ) parser.add_argument("--quiet", action="store_true", help="Minimizes webpack output while running") parser.add_argument( "--watch", action="store_true", help="watch for changes to source files (for development)" ) parser.add_argument( "--host", default="127.0.0.1", help="set the host for the webpack server to run on" ) parser.add_argument("--port", default="9994", help="set the port for the webpack server to run on") parser.add_argument( "--minify", action="store_true", help="Minify and optimize the assets (for development)" ) parser.add_argument( "--disable-host-check", action="store_true", help="Disable host check for webpack-dev-server" ) parser.add_argument("--config-name", help="Limit production building to only one config-name") args = parser.parse_args() if "PUPPETEER_TESTS" in os.environ: build_for_prod_or_puppeteer(args.quiet) elif args.test: build_for_most_tests() elif args.watch: build_for_dev_server(args.host, args.port, args.minify, args.disable_host_check) else: build_for_prod_or_puppeteer(args.quiet, args.config_name)