app/global.mjs (183 lines of code) (raw):
import { _, __, hashCode, setLoadingStage } from "util";
import * as Bugzilla from "bugzilla";
import * as Dialog from "dialog";
const g = {
appVersion: 1, // bump to force component reloading
nightly: undefined,
beta: undefined,
release: undefined,
components: undefined,
account: undefined,
products: [
"Core",
"DevTools",
"Developer Infrastructure",
"External Software Affecting Firefox",
"Fenix",
"Focus",
"Firefox Build System",
"Firefox for iOS",
"Firefox",
"GeckoView",
"NSPR",
"NSS",
"Remote Protocol",
"Release Engineering",
"Testing",
"Toolkit",
"Web Compatibility",
"WebExtensions",
],
};
export function allComponents() {
return g.components;
}
export function selectedComponents() {
const result = [];
for (const $cb of __("#components input:checked")) {
result.push(g.components.find((c) => c.id.toString() === $cb.id.slice(1)));
}
return result;
}
export function getAccount() {
return g.account;
}
export function setAccount(account) {
g.account = account;
}
export function releaseData() {
return {
nightly: g.nightly,
beta: g.beta,
release: g.release,
};
}
async function loadVersions() {
setLoadingStage("Firefox versions");
let response = await fetch(
"https://product-details.mozilla.org/1.0/firefox_versions.json?" + Date.now(),
);
let data = await response.json();
g.nightly = {};
g.nightly.version = data.FIREFOX_NIGHTLY.split(".")[0];
g.nightly.statusFlag = `cf_status_firefox${g.nightly.version}`;
g.beta = {};
g.beta.version = data.FIREFOX_DEVEDITION.split(".")[0];
g.beta.statusFlag = `cf_status_firefox${g.beta.version}`;
g.release = {};
g.release.version = data.LATEST_FIREFOX_VERSION.split(".")[0];
g.release.statusFlag = `cf_status_firefox${g.release.version}`;
setLoadingStage("Firefox releases");
response = await fetch(
"https://product-details.mozilla.org/1.0/firefox.json?" + Date.now(),
);
data = await response.json();
// load the versions, skipping beta, esr, and rc
const versions = {};
for (const entry of Object.entries(data.releases)) {
let versionStr = entry[0].replace(/^firefox-/, "");
if (Number(versionStr.split(".")[0]) <= 120) continue; // we can ignore old releases
if (/(?:\.\d+b\d+|esr|rc\d+)$/.test(versionStr)) continue;
if (versionStr.split(".").length === 2) {
versionStr = `${versionStr}.0`; // 129.0 --> 129.0.0
}
versions[versionStr] = entry[1].date;
}
// find the .0 release date, or the next following if that doesn't exist
for (const channel of ["beta", "release"]) {
const mergeVer = Number(g[channel].version) - 2;
let dot = 0;
while (dot <= 5) {
if (`${mergeVer}.0.${dot}` in versions) {
g[channel].date = versions[`${mergeVer}.0.${dot}`];
break;
}
dot++;
}
if (!g[channel].date) {
// eslint-disable-next-line no-console
console.error(`Failed to find merge date for ${channel}`);
document.body.classList.add("global-error");
}
}
/* eslint-disable no-console */
console.log("Nightly", g.nightly);
console.log("Beta", g.beta);
console.log("Release", g.release);
/* eslint-enable no-console */
}
async function loadComponents() {
// reload components once per month, or if the list of products or appVersion changes
const now = new Date();
const productsHash = hashCode(g.products.join("#") + g.appVersion.toString());
const currentCacheID = `${now.getFullYear()}.${now.getMonth()}:${productsHash}`;
const cacheID = window.localStorage.getItem("componentsID") || "";
const cacheData = window.localStorage.getItem("components");
if (cacheData && cacheID === currentCacheID) {
g.components = JSON.parse(cacheData);
return;
}
g.components = [];
for (const product of g.products) {
setLoadingStage(`Bugzilla product: ${product}`);
try {
const response = await Bugzilla.rest(
`product/${encodeURIComponent(product)}`,
{
// eslint-disable-next-line camelcase
include_fields:
"components.id,components.name,components.description,components.team_name",
},
);
if (response.products.length === 0) {
// eslint-disable-next-line no-console
console.error("Invalid product:", product);
document.body.classList.add("global-error");
continue;
}
for (const component of response.products[0].components) {
g.components.push({
id: component.id,
title: `${product}: ${component.name}`,
desc: component.description
.replaceAll(/<[^>]+>/g, " ")
.replaceAll("<", "<")
.replaceAll(">", ">"),
product: product,
component: component.name,
team: component.team_name,
});
}
} catch (error) {
await Dialog.alert(`Failed to load Bugzilla components: ${error}`);
return;
}
}
window.localStorage.setItem("componentsID", currentCacheID);
window.localStorage.setItem("components", JSON.stringify(g.components));
}
export async function clearComponentsCache() {
window.localStorage.setItem("componentsID", "");
}
export async function loadUser() {
const apiKey = Bugzilla.getApiKey();
if (apiKey.length === 0) {
g.account = undefined;
} else {
setLoadingStage("Bugzilla account");
g.account = await Bugzilla.whoami();
if (g.account === undefined) {
await Dialog.alert("Removing invalid Bugzilla API-Key.");
Bugzilla.setApiKey("");
}
}
}
export async function initData() {
_("#global-error").addEventListener("click", () =>
document.body.classList.add("egg"),
);
await loadUser();
await loadVersions();
await loadComponents();
setLoadingStage("");
}