Source code

Revision control

Copy as Markdown

Other Tools

/* 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 fs from "node:fs";
import path from "node:path";
import { fileURLToPath } from "node:url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
/* -------- paths -------- */
// Root of the `component-status` directory
const STATUS_ROOT = path.resolve(__dirname, "..");
// Root of the `firefox` repository
const REPO_ROOT = path.resolve(STATUS_ROOT, "../../..");
const STORIES_DIR = path.join(REPO_ROOT, "toolkit", "content", "widgets");
const BUGS_IDS_JSON = path.join(
STATUS_ROOT,
"component-status",
"data",
"bug-ids.json"
);
const OUT_JSON = path.join(STATUS_ROOT, "component-status", "components.json");
const PROD_STORYBOOK_URL =
globalThis?.process?.env?.PROD_STORYBOOK_URL ||
/* -------- data bug-ids -------- */
function readJsonIfExists(filePath) {
try {
if (fs.existsSync(filePath)) {
const txt = fs.readFileSync(filePath, "utf8");
return JSON.parse(txt);
}
} catch (e) {
console.error(`Error reading or parsing ${filePath}:`, e);
}
return {};
}
const BUG_IDS = readJsonIfExists(BUGS_IDS_JSON);
/* -------- helpers -------- */
function slugify(str) {
if (!str) {
return "";
}
let s = String(str).trim().toLowerCase();
s = s.replace(/[^a-z0-9]+/g, "-");
s = s.replace(/^-+|-+$/g, "");
s = s.replace(/--+/g, "-");
return s;
}
function getBugzillaUrl(bugId) {
return bugId && bugId > 0
: "";
}
function readFileSafe(file) {
try {
return fs.readFileSync(file, "utf8");
} catch (_e) {
return "";
}
}
function findStoriesFiles(dir) {
try {
return fs.readdirSync(dir, { withFileTypes: true }).flatMap(ent => {
const p = path.join(dir, ent.name);
if (ent.isDirectory()) {
return findStoriesFiles(p);
}
return ent.isFile() && /\.stories\.mjs$/i.test(ent.name) ? [p] : [];
});
} catch (e) {
console.error(`Error finding files in ${dir}:`, e);
return [];
}
}
// Parses `export default { title: "...", parameters: { status: "..." } }` from the file content
// Parses `export default { title: "...", parameters: { status: "..." } }`
function parseMeta(src) {
const meta = { title: "", status: "unknown" };
// First, find and capture the story's title
const titleMatch = src.match(
/export\s+default\s*\{\s*[\s\S]*?title\s*:\s*(['"`])([\s\S]*?)\1/
);
if (titleMatch && titleMatch[2]) {
meta.title = titleMatch[2].trim();
}
// Use the final "};" of the export as a definitive anchor to find the correct closing brace.
const paramsBlockMatch = src.match(
/parameters\s*:\s*(\{[\s\S]*?\})\s*,\s*};/
);
if (!paramsBlockMatch) {
return meta;
}
const paramsContent = paramsBlockMatch[1];
// Look for `status: "some-string"`
const stringStatusMatch = paramsContent.match(
/status\s*:\s*(['"`])([\s\S]*?)\1/
);
if (stringStatusMatch && stringStatusMatch[2]) {
meta.status = stringStatusMatch[2].trim().toLowerCase();
return meta;
}
// If a simple string wasn't found, look for `status: { type: "some-string" }`
const objectStatusMatch = paramsContent.match(
/status\s*:\s*\{\s*type\s*:\s*(['"`])([\s\S]*?)\1/
);
if (objectStatusMatch && objectStatusMatch[2]) {
meta.status = objectStatusMatch[2].trim().toLowerCase();
return meta;
}
return meta;
}
// Finds the main story export name (e.g., "Default" or the first export const)
function pickExportName(src) {
const names = [];
const re = /export\s+const\s+([A-Za-z0-9_]+)\s*=/g;
let m;
while ((m = re.exec(src))) {
names.push(m[1]);
}
if (names.length === 0) {
return "default";
}
for (const n of names) {
if (n.toLowerCase() === "default") {
return "default";
}
}
return names[0].toLowerCase();
}
function componentSlug(filePath, title) {
const rel = path.relative(STORIES_DIR, filePath);
const root = rel.split(path.sep)[0] || "";
if (root) {
return root;
}
const parts = title.split("/");
const last = parts[parts.length - 1].trim();
return slugify(last || "unknown");
}
/* -------- build items -------- */
function buildItems() {
const files = findStoriesFiles(STORIES_DIR);
const items = [];
for (const file of files) {
const src = readFileSafe(file);
if (!src) {
continue;
}
const meta = parseMeta(src);
if (!meta.title) {
continue;
}
const exportKey = pickExportName(src);
const titleSlug = slugify(meta.title);
const exportSlug = slugify(exportKey || "default");
if (!titleSlug || !exportSlug) {
continue;
}
const storyId = `${titleSlug}--${exportSlug}`;
const componentName = componentSlug(file, meta.title);
const storyUrl = `${PROD_STORYBOOK_URL}?path=/story/${storyId}`;
const sourceUrl = `https://searchfox.org/firefox-main/source/toolkit/content/widgets/${encodeURIComponent(componentName)}`;
const bugId = BUG_IDS[componentName] || 0;
const bugUrl = getBugzillaUrl(bugId);
items.push({
component: componentName,
title: meta.title,
status: meta.status,
storyId,
storyUrl,
sourceUrl,
bugUrl,
});
}
items.sort((a, b) => a.component.localeCompare(b.component));
return items;
}
/* -------- write JSON -------- */
const items = buildItems();
const data = {
generatedAt: new Date().toISOString(),
count: items.length,
items,
};
fs.writeFileSync(OUT_JSON, JSON.stringify(data, null, 2) + "\n");
console.warn(`wrote ${OUT_JSON} (${items.length} components)`);