mirror of
https://github.com/jimeh/commonflow.org.git
synced 2026-02-19 05:46:40 +00:00
228 lines
6.1 KiB
TypeScript
228 lines
6.1 KiB
TypeScript
/**
|
|
* Fetches spec documents and diagrams from the common-flow GitHub repo
|
|
* and writes them to the appropriate locations for Astro to consume.
|
|
*
|
|
* Versions are discovered from git tags and filtered based on config.
|
|
*/
|
|
|
|
import * as fs from "node:fs";
|
|
import * as path from "node:path";
|
|
import * as semver from "semver";
|
|
import { $ } from "bun";
|
|
import { config } from "../src/config";
|
|
|
|
const updateConfig = {
|
|
bodyTemplate: `---
|
|
title: {{title}}
|
|
version: {{version}}
|
|
---
|
|
{{content}}`,
|
|
outputDir: "src/content/spec",
|
|
publicDir: "public/spec",
|
|
};
|
|
|
|
/**
|
|
* Fetch all tags from the GitHub repository.
|
|
*/
|
|
async function fetchTags(repository: string): Promise<string[]> {
|
|
const repoUrl = `https://github.com/${repository}.git`;
|
|
console.log(`Fetching tags from ${repoUrl}...`);
|
|
|
|
try {
|
|
const result = await $`git ls-remote --tags ${repoUrl}`.text();
|
|
|
|
return result
|
|
.split("\n")
|
|
.filter(Boolean)
|
|
.map((line: string) => line.match(/refs\/tags\/(.+)$/)?.[1])
|
|
.filter(
|
|
(tag: string | undefined): tag is string =>
|
|
tag !== undefined && !tag.endsWith("^{}"),
|
|
);
|
|
} catch (error) {
|
|
throw new Error(
|
|
`Failed to fetch tags: ${error instanceof Error ? error.message : String(error)}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the prerelease type of a version (e.g., "rc", "draft", or null for
|
|
* stable).
|
|
*/
|
|
function getPrereleaseType(version: string): string | null {
|
|
const prerelease = semver.prerelease(version);
|
|
if (!prerelease) return null;
|
|
return String(prerelease[0]);
|
|
}
|
|
|
|
/**
|
|
* Filter tags based on discovery configuration.
|
|
* Stable versions are always included; prereleases only if their type is in
|
|
* the includePrereleaseTypes list.
|
|
*/
|
|
function filterVersions(tags: string[]): string[] {
|
|
const { includePrereleaseTypes, excludeVersions } = config.update.discovery;
|
|
|
|
return tags.filter((tag) => {
|
|
// Must be valid semver
|
|
if (!semver.valid(tag)) return false;
|
|
|
|
// Check explicit exclusions
|
|
if (excludeVersions.includes(tag)) return false;
|
|
|
|
// Stable versions are always included
|
|
const prereleaseType = getPrereleaseType(tag);
|
|
if (prereleaseType === null) return true;
|
|
|
|
// Prereleases only if their type is in the list
|
|
return includePrereleaseTypes.includes(prereleaseType);
|
|
});
|
|
}
|
|
|
|
function buildFileUrl(
|
|
fileType: "document" | "diagram",
|
|
version: string,
|
|
): string {
|
|
const file = config.update.files[fileType];
|
|
return config.update.urlTemplate
|
|
.replace("{{version}}", version)
|
|
.replace("{{file}}", file);
|
|
}
|
|
|
|
async function fetchFile(url: string): Promise<string | null> {
|
|
try {
|
|
const response = await fetch(url);
|
|
if (!response.ok) {
|
|
return null;
|
|
}
|
|
return await response.text();
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function writeFile(filePath: string, content: string, comment = ""): void {
|
|
const dir = path.dirname(filePath);
|
|
if (!fs.existsSync(dir)) {
|
|
fs.mkdirSync(dir, { recursive: true });
|
|
}
|
|
fs.writeFileSync(filePath, content);
|
|
console.log(` - ${filePath}${comment}`);
|
|
}
|
|
|
|
/**
|
|
* Remove spec files for versions not in the provided list.
|
|
* Files for versions in the list are left alone (they'll be overwritten).
|
|
*/
|
|
function removeStaleSpecs(versionsToKeep: string[]): void {
|
|
const keepSet = new Set(versionsToKeep);
|
|
let removedAny = false;
|
|
|
|
for (const dir of [updateConfig.outputDir, updateConfig.publicDir]) {
|
|
if (!fs.existsSync(dir)) continue;
|
|
|
|
const files = fs.readdirSync(dir);
|
|
for (const file of files) {
|
|
// Extract version from filename (e.g., "1.0.0-rc.1.md" -> "1.0.0-rc.1")
|
|
const version = path.basename(file, path.extname(file));
|
|
if (!keepSet.has(version)) {
|
|
if (!removedAny) {
|
|
console.log("\nRemoving stale spec files:");
|
|
removedAny = true;
|
|
}
|
|
const filePath = path.join(dir, file);
|
|
fs.unlinkSync(filePath);
|
|
console.log(` ${filePath}`);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!removedAny) {
|
|
console.log("\nNo stale spec files to remove.");
|
|
}
|
|
}
|
|
|
|
interface Spec {
|
|
version: string;
|
|
title: string;
|
|
body: string;
|
|
diagram: string | null;
|
|
}
|
|
|
|
async function fetchSpec(version: string): Promise<Spec> {
|
|
const documentUrl = buildFileUrl("document", version);
|
|
const diagramUrl = buildFileUrl("diagram", version);
|
|
|
|
let document = await fetchFile(documentUrl);
|
|
const diagram = await fetchFile(diagramUrl);
|
|
|
|
if (!document) {
|
|
throw new Error(`Failed to fetch document for version ${version}`);
|
|
}
|
|
|
|
// Replace {{version}} placeholder throughout the document
|
|
document = document.replaceAll("{{version}}", version);
|
|
|
|
// Extract title from first line (after version replacement)
|
|
const title = document.split("\n", 1)[0];
|
|
|
|
// Build body with frontmatter
|
|
const body = updateConfig.bodyTemplate
|
|
.replace("{{content}}", document)
|
|
.replace("{{title}}", title)
|
|
.replace("{{version}}", version);
|
|
|
|
return {
|
|
version,
|
|
title,
|
|
body,
|
|
diagram,
|
|
};
|
|
}
|
|
|
|
async function main(): Promise<void> {
|
|
// 1. Discover and filter versions
|
|
const tags = await fetchTags(config.update.repository);
|
|
console.log(`Found ${tags.length} tags`);
|
|
|
|
const filtered = filterVersions(tags);
|
|
const sorted = semver.rsort([...filtered]);
|
|
|
|
console.log(`\nIncluded ${sorted.length} versions after filtering:`);
|
|
console.log(` ${sorted.join(", ")}`);
|
|
|
|
if (sorted.length === 0) {
|
|
console.error("\nNo versions to process. Exiting.");
|
|
process.exit(1);
|
|
}
|
|
|
|
// 2. Remove spec files for versions no longer in the list
|
|
removeStaleSpecs(sorted);
|
|
|
|
// 3. Fetch specs for all versions
|
|
console.log("\nFetching spec documents:");
|
|
|
|
for (const version of sorted) {
|
|
try {
|
|
const spec = await fetchSpec(version);
|
|
|
|
// Write markdown file to content collection
|
|
const mdPath = path.join(updateConfig.outputDir, `${version}.md`);
|
|
writeFile(mdPath, spec.body);
|
|
|
|
// Write SVG diagram to public directory
|
|
if (spec.diagram) {
|
|
const svgPath = path.join(updateConfig.publicDir, `${version}.svg`);
|
|
writeFile(svgPath, spec.diagram);
|
|
}
|
|
} catch (error) {
|
|
console.error(`Error processing version ${version}:`, error);
|
|
}
|
|
}
|
|
|
|
console.log("\nDone! Run `bun run build` to rebuild the site.");
|
|
}
|
|
|
|
main();
|