#!/usr/bin/env -S deno run --allow-env --allow-read --allow-write --allow-net import { Ajv, parseYaml, path, PortCategories, portsSchema, schema, } from "./deps.ts"; import { ApplicationLink, CurrentMaintainers, FAQ, Name, PastMaintainers, Usage, Userstyle, Userstyles, } from "./types.d.ts"; const ROOT = new URL(".", import.meta.url).pathname; const REPO_ROOT = path.join(ROOT, "../.."); const ISSUE_PREFIX = "lbl:"; type Metadata = { userstyles: Userstyles; }; type PortMetadata = { categories: PortCategories; }; type CollaboratorsData = { collaborators: CurrentMaintainers | PastMaintainers; heading: string; }; export type MappedPort = Userstyle & { path: string }; const ajv = new (Ajv as unknown as (typeof Ajv)["default"])(); const validate = ajv.compile(schema); const validatePorts = ajv.compile(portsSchema); const userstylesYaml = Deno.readTextFileSync( path.join(ROOT, "../userstyles.yml"), ); const userstylesData = parseYaml(userstylesYaml); if (!validate(userstylesData)) { console.log(validate.errors); Deno.exit(1); } const portsYaml = await fetch( "https://raw.githubusercontent.com/catppuccin/catppuccin/main/resources/ports.yml", ); const portsData = parseYaml(await portsYaml.text()); if (!validatePorts(portsData)) { console.log(validate.errors); Deno.exit(1); } const categorized = Object.entries(userstylesData.userstyles).reduce( (acc, [slug, { category, ...port }]) => { acc[category] ||= []; acc[category].push({ path: `styles/${slug}`, category, ...port }); acc[category].sort((a, b) => { const aName = typeof a.name === "string" ? a.name : a.name.join(", "); const bName = typeof b.name === "string" ? b.name : b.name.join(", "); return aName.localeCompare(bName); }); return acc; }, {} as Record, ); const portListData = portsData.categories .filter((category) => categorized[category.key] !== undefined) .map((category) => { return { meta: category, ports: categorized[category.key], }; }); const portContent = portListData .map( (data) => `
${data.meta.emoji} ${data.meta.name} ${ data.ports .map((port) => { const name = Array.isArray(port.name) ? port.name.join(", ") : port.name; return `- [${name}](${port.path})`; }) .join("\n") }
`, ) .join("\n"); const updateReadme = ({ readme, section, newContent, }: { readme: string; section: string; newContent: string; }): string => { const preamble = ""; const startMarker = ``; const endMarker = ``; const wrapped = `${startMarker}\n${preamble}\n${newContent}\n${endMarker}`; if ( !(readmeContent.includes(startMarker) && readmeContent.includes(endMarker)) ) { throw new Error("Markers not found in README.md"); } const pre = readme.split(startMarker)[0]; const end = readme.split(endMarker)[1]; return pre + wrapped + end; }; const updateFile = (filePath: string, fileContent: string, comment = true) => { const preamble = comment ? "# THIS FILE IS AUTOGENERATED. DO NOT EDIT IT BY HAND.\n" : ""; Deno.writeTextFileSync(filePath, preamble + fileContent); }; const readmePath = path.join(REPO_ROOT, "README.md"); let readmeContent = Deno.readTextFileSync(readmePath); try { readmeContent = updateReadme({ readme: readmeContent, section: "userstyles", newContent: portContent, }); updateFile(readmePath, readmeContent, false); } catch (e) { console.log("Failed to update the README:", e); } const pullRequestLabelerPath = path.join(REPO_ROOT, ".github/pr-labeler.yml"); const pullRequestLabelerContent = Object.entries(userstylesData.userstyles) .map(([key]) => `${key}: styles/${key}/**/*`) .join("\n"); updateFile(pullRequestLabelerPath, pullRequestLabelerContent); const issuesLabelerPath = path.join(REPO_ROOT, ".github/issue-labeler.yml"); const issuesLabelerContent = Object.entries(userstylesData.userstyles) .map(([key]) => `${key}: ['(${ISSUE_PREFIX + key})']`) .join("\n"); updateFile(issuesLabelerPath, issuesLabelerContent); const ownersPath = path.join(REPO_ROOT, ".github/CODEOWNERS"); const ownersContent = Object.entries(userstylesData.userstyles) .map(([key, style]) => { const currentMaintainers = style.readme["current-maintainers"] .map((maintainer) => `@${maintainer.url.split("/").pop()}`) .join(" "); return `/styles/${key} ${currentMaintainers}`; }) .join("\n"); updateFile(ownersPath, ownersContent); const userstyleIssuePath = path.join(ROOT, "templates/userstyle-issue.yml"); const userstyleIssueContent = Deno.readTextFileSync(userstyleIssuePath); const replacedUserstyleIssueContent = userstyleIssueContent.replace( "$PORTS", `${ Object.entries(userstylesData.userstyles) .map(([key]) => `'${ISSUE_PREFIX + key}'`) .join(", ") }`, ); Deno.writeTextFileSync( path.join(REPO_ROOT, ".github/ISSUE_TEMPLATE/userstyle.yml"), replacedUserstyleIssueContent, ); const heading = (name: Name, link: ApplicationLink) => { const nameArray = Array.isArray(name) ? name : [name]; const linkArray = Array.isArray(link) ? link : [link]; if (nameArray.length !== linkArray.length) { throw new Error( 'The "name" and "app-link" arrays must have the same length', ); } return `Catppuccin for ${ nameArray .map((name, index) => `${name}`) .join(", ") }`; }; const usageContent = (usage?: Usage) => { return !usage ? "" : `## Usage \n${usage}`; }; const faqContent = (faq?: FAQ) => { if (!faq) { return ""; } return `## 🙋 FAQ ${ faq .map(({ question, answer }) => `- Q: ${question} \n\tA: ${answer}`) .join("\n") }`; }; const collaboratorsContent = (allCollaborators: CollaboratorsData[]) => { return allCollaborators .filter(({ collaborators }) => collaborators !== undefined) .map(({ collaborators, heading }) => { const collaboratorBody = collaborators .map(({ name, url }) => `- [${name ?? url.split("/").pop()}](${url})`) .join("\n"); return `${heading}\n${collaboratorBody}`; }) .join("\n\n"); }; const updateStylesReadmeContent = ( readme: string, key: string, userstyle: Userstyle, ) => { return readme .replace("$TITLE", heading(userstyle.name, userstyle.readme["app-link"])) .replaceAll("$LOWERCASE-PORT", key) .replace("$USAGE", usageContent(userstyle.readme.usage)) .replace("$FAQ", faqContent(userstyle.readme.faq)) .replace( "$COLLABORATORS", collaboratorsContent([ { collaborators: userstyle.readme["current-maintainers"], heading: "## 💝 Current Maintainer(s)", }, { collaborators: userstyle.readme["past-maintainers"], heading: "## 💖 Past Maintainer(s)", }, ]), ); }; const stylesReadmePath = path.join(ROOT, "templates/userstyle.md"); const stylesReadmeContent = Deno.readTextFileSync(stylesReadmePath); for (const [key, userstyle] of Object.entries(userstylesData.userstyles)) { try { console.log(`Generating README for ${key}`); readmeContent = updateStylesReadmeContent( stylesReadmeContent, key, userstyle, ); Deno.writeTextFileSync( path.join(REPO_ROOT, "styles", key, "README.md"), readmeContent, ); } catch (e) { console.log(`Failed to update ${userstyle} README:`, e); } }