279 lines
7.7 KiB
TypeScript
Executable File

#!/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<Metadata>(schema);
const validatePorts = ajv.compile<PortMetadata>(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<string, MappedPort[]>
);
const portListData = portsData.categories
.filter((category) => categorized[category.key] !== undefined)
.map((category) => {
return {
meta: category,
ports: categorized[category.key],
};
});
const portContent = portListData
.map(
(data) =>
`<details open>
<summary>${data.meta.emoji} ${data.meta.name}</summary>
${data.ports
.map((port) => {
const name = Array.isArray(port.name) ? port.name.join(", ") : port.name;
return `- [${name}](${port.path})`;
})
.join("\n")}
</details>`
)
.join("\n");
const updateReadme = ({
readme,
section,
newContent,
}: {
readme: string;
section: string;
newContent: string;
}): string => {
const preamble =
"<!-- the following section is auto-generated, do not edit -->";
const startMarker = `<!-- AUTOGEN:${section.toUpperCase()} START -->`;
const endMarker = `<!-- AUTOGEN:${section.toUpperCase()} END -->`;
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 syncLabels = path.join(REPO_ROOT, ".github/labels.yml");
const syncLabelsContent = Object.entries(userstylesData.userstyles)
.map(
([key, style]) =>
`- name: ${key}
description: ${style.name}
color: "#8aadf4"`
)
.join("\n");
updateFile(syncLabels, syncLabelsContent);
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) => `<a href="${linkArray[index]}">${name}</a>`)
.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);
}
}