tor-browser

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

build-components-status.mjs (5852B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 import fs from "node:fs";
      6 import path from "node:path";
      7 import { fileURLToPath } from "node:url";
      8 
      9 const __filename = fileURLToPath(import.meta.url);
     10 const __dirname = path.dirname(__filename);
     11 
     12 /* -------- paths -------- */
     13 
     14 // Root of the `component-status` directory
     15 const STATUS_ROOT = path.resolve(__dirname, "..");
     16 // Root of the `firefox` repository
     17 const REPO_ROOT = path.resolve(STATUS_ROOT, "../../..");
     18 
     19 const STORIES_DIR = path.join(REPO_ROOT, "toolkit", "content", "widgets");
     20 const BUGS_IDS_JSON = path.join(
     21  STATUS_ROOT,
     22  "component-status",
     23  "data",
     24  "bug-ids.json"
     25 );
     26 const OUT_JSON = path.join(STATUS_ROOT, "component-status", "components.json");
     27 
     28 const PROD_STORYBOOK_URL =
     29  globalThis?.process?.env?.PROD_STORYBOOK_URL ||
     30  "https://firefoxux.github.io/firefox-desktop-components/";
     31 
     32 /* -------- data bug-ids -------- */
     33 
     34 function readJsonIfExists(filePath) {
     35  try {
     36    if (fs.existsSync(filePath)) {
     37      const txt = fs.readFileSync(filePath, "utf8");
     38      return JSON.parse(txt);
     39    }
     40  } catch (e) {
     41    console.error(`Error reading or parsing ${filePath}:`, e);
     42  }
     43  return {};
     44 }
     45 
     46 const BUG_IDS = readJsonIfExists(BUGS_IDS_JSON);
     47 
     48 /* -------- helpers -------- */
     49 
     50 function slugify(str) {
     51  if (!str) {
     52    return "";
     53  }
     54  let s = String(str).trim().toLowerCase();
     55  s = s.replace(/[^a-z0-9]+/g, "-");
     56  s = s.replace(/^-+|-+$/g, "");
     57  s = s.replace(/--+/g, "-");
     58  return s;
     59 }
     60 
     61 function getBugzillaUrl(bugId) {
     62  return bugId && bugId > 0
     63    ? `https://bugzilla.mozilla.org/show_bug.cgi?id=${bugId}`
     64    : "";
     65 }
     66 
     67 function readFileSafe(file) {
     68  try {
     69    return fs.readFileSync(file, "utf8");
     70  } catch (_e) {
     71    return "";
     72  }
     73 }
     74 
     75 function findStoriesFiles(dir) {
     76  try {
     77    return fs.readdirSync(dir, { withFileTypes: true }).flatMap(ent => {
     78      const p = path.join(dir, ent.name);
     79      if (ent.isDirectory()) {
     80        return findStoriesFiles(p);
     81      }
     82      return ent.isFile() && /\.stories\.mjs$/i.test(ent.name) ? [p] : [];
     83    });
     84  } catch (e) {
     85    console.error(`Error finding files in ${dir}:`, e);
     86    return [];
     87  }
     88 }
     89 
     90 // Parses `export default { title: "...", parameters: { status: "..." } }` from the file content
     91 // Parses `export default { title: "...", parameters: { status: "..." } }`
     92 function parseMeta(src) {
     93  const meta = { title: "", status: "unknown" };
     94 
     95  // First, find and capture the story's title
     96  const titleMatch = src.match(
     97    /export\s+default\s*\{\s*[\s\S]*?title\s*:\s*(['"`])([\s\S]*?)\1/
     98  );
     99  if (titleMatch && titleMatch[2]) {
    100    meta.title = titleMatch[2].trim();
    101  }
    102 
    103  // Use the final "};" of the export as a definitive anchor to find the correct closing brace.
    104  const paramsBlockMatch = src.match(
    105    /parameters\s*:\s*(\{[\s\S]*?\})\s*,\s*};/
    106  );
    107 
    108  if (!paramsBlockMatch) {
    109    return meta;
    110  }
    111  const paramsContent = paramsBlockMatch[1];
    112 
    113  // Look for `status: "some-string"`
    114  const stringStatusMatch = paramsContent.match(
    115    /status\s*:\s*(['"`])([\s\S]*?)\1/
    116  );
    117  if (stringStatusMatch && stringStatusMatch[2]) {
    118    meta.status = stringStatusMatch[2].trim().toLowerCase();
    119    return meta;
    120  }
    121 
    122  // If a simple string wasn't found, look for `status: { type: "some-string" }`
    123  const objectStatusMatch = paramsContent.match(
    124    /status\s*:\s*\{\s*type\s*:\s*(['"`])([\s\S]*?)\1/
    125  );
    126  if (objectStatusMatch && objectStatusMatch[2]) {
    127    meta.status = objectStatusMatch[2].trim().toLowerCase();
    128    return meta;
    129  }
    130 
    131  return meta;
    132 }
    133 
    134 // Finds the main story export name (e.g., "Default" or the first export const)
    135 function pickExportName(src) {
    136  const names = [];
    137  const re = /export\s+const\s+([A-Za-z0-9_]+)\s*=/g;
    138  let m;
    139  while ((m = re.exec(src))) {
    140    names.push(m[1]);
    141  }
    142  if (names.length === 0) {
    143    return "default";
    144  }
    145  for (const n of names) {
    146    if (n.toLowerCase() === "default") {
    147      return "default";
    148    }
    149  }
    150  return names[0].toLowerCase();
    151 }
    152 
    153 function componentSlug(filePath, title) {
    154  const rel = path.relative(STORIES_DIR, filePath);
    155  const root = rel.split(path.sep)[0] || "";
    156  if (root) {
    157    return root;
    158  }
    159  const parts = title.split("/");
    160  const last = parts[parts.length - 1].trim();
    161  return slugify(last || "unknown");
    162 }
    163 
    164 /* -------- build items -------- */
    165 function buildItems() {
    166  const files = findStoriesFiles(STORIES_DIR);
    167  const items = [];
    168 
    169  for (const file of files) {
    170    const src = readFileSafe(file);
    171    if (!src) {
    172      continue;
    173    }
    174 
    175    const meta = parseMeta(src);
    176    if (!meta.title) {
    177      continue;
    178    }
    179 
    180    const exportKey = pickExportName(src);
    181    const titleSlug = slugify(meta.title);
    182    const exportSlug = slugify(exportKey || "default");
    183    if (!titleSlug || !exportSlug) {
    184      continue;
    185    }
    186 
    187    const storyId = `${titleSlug}--${exportSlug}`;
    188    const componentName = componentSlug(file, meta.title);
    189 
    190    const storyUrl = `${PROD_STORYBOOK_URL}?path=/story/${storyId}`;
    191    const sourceUrl = `https://searchfox.org/firefox-main/source/toolkit/content/widgets/${encodeURIComponent(componentName)}`;
    192 
    193    const bugId = BUG_IDS[componentName] || 0;
    194    const bugUrl = getBugzillaUrl(bugId);
    195 
    196    items.push({
    197      component: componentName,
    198      title: meta.title,
    199      status: meta.status,
    200      storyId,
    201      storyUrl,
    202      sourceUrl,
    203      bugUrl,
    204    });
    205  }
    206 
    207  items.sort((a, b) => a.component.localeCompare(b.component));
    208  return items;
    209 }
    210 
    211 /* -------- write JSON -------- */
    212 
    213 const items = buildItems();
    214 const data = {
    215  generatedAt: new Date().toISOString(),
    216  count: items.length,
    217  items,
    218 };
    219 
    220 fs.writeFileSync(OUT_JSON, JSON.stringify(data, null, 2) + "\n");
    221 console.warn(`wrote ${OUT_JSON} (${items.length} components)`);