Skip to content

Commit 577c80a

Browse files
Sandra Quhannajones
authored andcommitted
Bug 1854696 Adding a Storybook Component Status Page to /docs r=desktop-theme-reviewers,hjones
Differential Revision: https://phabricator.services.mozilla.com/D264673
1 parent 655a086 commit 577c80a

File tree

7 files changed

+940
-3
lines changed

7 files changed

+940
-3
lines changed

browser/components/storybook/.storybook/main.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ module.exports = {
1515
stories: [
1616
// Show the Storybook document first in the list
1717
// so that navigating to firefoxux.github.io/firefox-desktop-components/
18-
// lands on the Storybook.stories.md file
18+
// lands on the ComponentStatus.stories.md file
19+
`../**/component-status.stories.mjs`,
20+
// and lands on the Storybook.stories.md file
1921
"../**/README.storybook.stories.md",
2022
// Docs section
2123
"../**/README.*.stories.md",
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
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)`);

0 commit comments

Comments
 (0)