# 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 os import shutil import sys import mozpack.path as mozpath from mach.decorators import Command, CommandArgument, SubCommand targets = ["dom", "nsresult", "services", "xpcom"] @Command("ts", category="misc", description="Run TypeScript and related commands.") def ts(ctx): """ TypeScript related commands to build/update typelibs and type-check js. Example: # Use tsc to check types in extensions framework code: $ ./mach ts check toolkit/components/extensions """ ctx._sub_mach(["help", "ts"]) return 1 @SubCommand("ts", "build", description="Build typelibs.") @CommandArgument("lib", choices=targets, nargs="?") def build(ctx, lib): """Command to build the target typelibs.""" if lib is None: for t in targets: if rv := build(ctx, t): return rv return 0 types_dir = mozpath.join(ctx.distdir, "@types") lib_dts = mozpath.join(types_dir, f"lib.gecko.{lib}.d.ts") if not os.path.exists(types_dir): os.makedirs(types_dir) if lib == "nsresult": xpc_msg = mozpath.join(ctx.topsrcdir, "js/xpconnect/src/xpc.msg") errors_json = mozpath.join(ctx.topsrcdir, "tools/ts/config/error_list.json") return node(ctx, "build_nsresult", lib_dts, xpc_msg, errors_json) if lib == "services": services_json = mozpath.join(ctx.topobjdir, "xpcom/components/services.json") if not os.path.isfile(services_json): return build_required(lib, services_json) return node(ctx, "build_services", lib_dts, services_json) if lib == "xpcom": # When we hook this up to be part of the build, we'll have # an explicit list of files. For now, just get all of them. dir = mozpath.join(ctx.topobjdir, "config/makefiles/xpidl") if not os.path.isdir(dir): return build_required(lib, dir) files = [f for f in os.listdir(dir) if f.endswith(".d.json")] if not files: return build_required(lib, f"*.d.json files in {dir}") return node(ctx, "build_xpcom", lib_dts, dir, *files) if lib == "dom": # Same as above, get all *.webidl files for now. dir = mozpath.join(ctx.topsrcdir, "dom") files = [] for subdir in ["webidl", "chrome-webidl"]: for file in os.listdir(mozpath.join(dir, subdir)): if file.endswith(".webidl"): files.append(subdir + "/" + file) return node(ctx, "build_dom", lib_dts, dir, *files) raise ValueError(f"Unknown typelib: {lib}") @SubCommand("ts", "check", description="Check types in a project using tsc.") @CommandArgument("paths", nargs="+", help="Path to a (dir with) tsconfig.json.") def check(ctx, paths): for p in paths: rv = node(ctx, "node_modules/typescript/bin/tsc", "--project", p) if rv: return rv return 0 @SubCommand("ts", "setup", description="Install TypeScript and other dependencies.") def setup(ctx): # Install locally under tools/ts/node_modules, to avoid any conflicts for now. os.chdir(mozpath.dirname(__file__)) return ctx._sub_mach(["npm", "ci"]) @SubCommand("ts", "update", description="Update tools/@types libraries.") def update(ctx): typelib_dir = mozpath.join(ctx.topsrcdir, "tools/@types/generated") platforms = ["darwin", "linux", "win32"] for lib in targets + platforms: file = f"lib.gecko.{lib}.d.ts" path = mozpath.join(ctx.distdir, "@types", file) if not os.path.exists(path): if lib in platforms: continue print(f"[ERROR] {path} not found. Did you run `mach ts build`?") return 1 # This command inherently goes in a confusing direction, we're copying: # from `/dist/@types` files generated with `mach ts build`, # into `/tools/@types` typelib dir back in your source tree. print(f"[INFO] Updating {typelib_dir}/{file}") shutil.copy(path, typelib_dir) print("[WARNING] Your source tree was updated, you should commit the changes.") @SubCommand("ts", "glean", description="Build Glean bindings.") def glean(ctx): sys.path.append(mozpath.join(ctx.topsrcdir, "toolkit/components/glean/")) from metrics_index import metrics_yamls, pings_yamls typelib_dir = mozpath.join(ctx.topsrcdir, "tools/@types/generated") maybe_setup(ctx) return node( ctx, "build_glean", ctx.topsrcdir, typelib_dir, *metrics_yamls, *pings_yamls ) @SubCommand("ts", "paths", description="Build module path mapping.") def paths(ctx): maybe_setup(ctx) lib = mozpath.join(ctx.topsrcdir, "tools/@types/generated/tspaths.json") lazy = mozpath.join(ctx.topsrcdir, "tools/@types/generated/lib.gecko.modules.d.ts") return node(ctx, "build_paths", ctx.topsrcdir, lib, lazy) @SubCommand("ts", "subs", description="Emit substitution .d.ts for processed sources.") def subs(ctx): maybe_setup(ctx) processed = [ # AppConstants.sys.mjs has a (better) manually created declaration file. "dist/bin/browser/modules/policies/schema.sys.mjs", "dist/bin/modules/Readerable.sys.mjs", "toolkit/components/nimbus/FeatureManifest.sys.mjs", "toolkit/components/promiseworker/worker/PromiseWorker.js", "toolkit/components/promiseworker/worker/PromiseWorker.mjs", "toolkit/components/resistfingerprinting/RFPTargetConstants.sys.mjs", ] args = ["--declaration", "--emitDeclarationOnly", "--allowJs", "--outDir"] subs_dir = mozpath.join(ctx.topsrcdir, "tools/@types/subs") for file in processed: path = mozpath.join(ctx.topobjdir, file) print(f"[INFO] {path} -> {subs_dir}/{mozpath.basename(path)}") node(ctx, "node_modules/typescript/bin/tsc", *args, subs_dir, path) def node(ctx, script, *args): maybe_setup(ctx) path = mozpath.join(mozpath.dirname(__file__), script) return ctx._sub_mach(["node", path, *args]) def maybe_setup(ctx): sys.path.append(mozpath.join(ctx.topsrcdir, "tools", "lint", "eslint")) import setup_helper if not setup_helper.check_node_executables_valid(): return 1 """Check if npm modules are installed, and run setup if needed.""" dir = mozpath.dirname(__file__) setup_helper.eslint_maybe_setup(dir, "TypeScript") def build_required(lib, item): print(f"Missing {item}.") print(f"Building {lib} typelib requires a full Firefox build.") return 1