Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add support for multi versions in docs #393

Open
wants to merge 2 commits into
base: v2-astro
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
212 changes: 128 additions & 84 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@
"name": "grain-lang.org",
"type": "module",
"version": "0.0.0",
"grainVersion": "v0.6.6",
"private": true,
"scripts": {
"prepare": "node scripts/generateContributors.js && node scripts/cli.mjs",
"docs": "node --experimental-strip-types scripts/docs.ts",
"dev": "astro dev",
"start": "astro dev",
"build": "astro build",
Expand Down Expand Up @@ -55,6 +57,7 @@
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.12",
"@types/node": "^22.10.10",
"@types/ramda": "^0.30.2"
}
}
99 changes: 99 additions & 0 deletions scripts/docs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { Octokit } from "octokit";
import os from "node:os";
import fs from "node:fs";
import path from "node:path";
import url from "node:url";

// Constants
const GRAIN_ORG = "grain-lang";
const GRAIN_REPO = "grain";
const GRAIN_DOCS_PATH = "stdlib";
const VERSION_SKIP_LIST = ["v0.3.0", "v0.3.1", "v0.3.2"];
const FILE_SKIP_LIST = ["stdlib/README.md", "stdlib/CHANGELOG.md", "stdlib/runtime/"]

const cwd = path.dirname(url.fileURLToPath(import.meta.url));

if ('GITHUB_ACCESS_TOKEN' in process.env) {
console.log("No Github access token found, rate limits may be hit.");
}
const auth = 'GITHUB_ACCESS_TOKEN' in process.env ? process.env.GITHUB_ACCESS_TOKEN : "";
const octokit = new Octokit({ auth });

const collectReleases = async (octokit: Octokit, page: number = 1) => {
// TODO: Handle quota limits
const {data, headers} = await octokit.rest.repos.listReleases({
owner: GRAIN_ORG,
repo: GRAIN_REPO,
page
});
// Filter releases
const releaseList = data.filter(({ tag_name }) => tag_name.startsWith("grain-") || tag_name == "preview");
// Fetch next page if available
const linkHeader = headers.link;
const pagesRemaining = linkHeader && linkHeader.includes(`rel=\"next\"`);
if (pagesRemaining) {
releaseList.push(...await collectReleases(octokit, page + 1));
}
return releaseList;
}

const collectDocFiles = async (
octokit: Octokit,
ref: string,
path: string
): Promise<{ name: string, path: string, download_url: string | null }[]> => {
// Fetch path content
let {data} = await octokit.rest.repos.getContent({
owner: GRAIN_ORG,
repo: GRAIN_REPO,
path,
ref
});
const directoryContent = Array.isArray(data) ? data : [data];
const documentList = [];
for await (const entry of directoryContent) {
if (entry.type == "file") {
if (!entry.name.endsWith(".md")) continue;
documentList.push(entry);
} else if (entry.type == "dir") {
// Recursively fetch directory content
const dirContent = await collectDocFiles(octokit, ref, entry.path);
documentList.push(...dirContent);
}
}
return documentList;
}

const grainReleases = await collectReleases(octokit);
const versionList: string[] = [];
// Note: Using Promise.all so that we can fetch all releases asynchronously
await Promise.all(grainReleases.map(async (release) => {
const version = release.tag_name.replace("grain-", "");
if (VERSION_SKIP_LIST.includes(version)) return;
versionList.push(version);
const docsPath = path.join(cwd, "../src/content/docs/", version);
// Only fetch docs if they don't exist
const docsExist = fs.existsSync(docsPath);
if (version != "preview" && docsExist) return;
console.log("Fetching docs for release:", release.tag_name);
const docFiles = await collectDocFiles(octokit, release.target_commitish, GRAIN_DOCS_PATH);
if (docsExist) await fs.promises.rm(docsPath, { recursive: true });
await fs.promises.mkdir(docsPath, { recursive: true });
await Promise.all(docFiles.map(async (file) => {
if (FILE_SKIP_LIST.some(prefix => file.path.startsWith(prefix))) return;
if (!file.download_url) {
console.log(`No download url for file: ${file} in version ${version}`);
return;
}
const response = await fetch(file.download_url);
if (response.status != 200) {
console.log(`Failed to download file: ${file} in version ${version}`);
return;
}
const content = await response.text();
const docPath = path.join(docsPath, file.path);
await fs.promises.mkdir(path.dirname(docPath), { recursive: true });
await fs.promises.writeFile(docPath, content);
}));
}));
await fs.promises.writeFile(path.join(cwd, "../src/content/docs/versions.json"), JSON.stringify(versionList.sort()));
7 changes: 4 additions & 3 deletions src/components/DocLink.astro
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
---
interface Props {
id: string;
version: string;
slug: string;
}

const { id } = Astro.props;
const { version, slug } = Astro.props;

const href = `/docs/${id}`;
const href = `/docs/${version}/${slug}`;
const isCurrentPage = Astro.url.pathname.replace(/\/$/, "") === href.replace(/\/$/, "");
---

Expand Down
22 changes: 13 additions & 9 deletions src/components/DocLinkGroup.astro
Original file line number Diff line number Diff line change
@@ -1,28 +1,32 @@
---
import type { CollectionEntry } from "astro:content";
import type { DocEntry } from "../utils/docsCollections";
import DocLink from "./DocLink.astro";
import path from "node:path";

interface Props {
version: string;
title: string | null;
entries: CollectionEntry<"docs">[]
entries: DocEntry[]
}

const { title, entries } = Astro.props;
const { version, title, entries } = Astro.props;
---

{title && (
<h2 class="text-xl font-semibold mt-6 mb-2">{title}</h2>
)}
<div class="border-l-2 border-color-dim-3">
{entries.map(x => {
const pathParts = x.id.split(path.sep);
const pathPrefixes = pathParts.slice(1, -1);
return path.dirname(x.id) !== pathParts[0] && pathPrefixes.length > 0
? (
<DocLink id={x.id}><span class="text-color-dim-1">{pathPrefixes.join("/")} / </span><span>{x.data.title}</span></DocLink>
{entries.map(entry => {
const pathParts = entry.title.split(path.sep);
const pathPrefixes = pathParts.slice(0, -1);
const isRoot = pathPrefixes.length === 0;
return !isRoot ? (
<DocLink version={version} slug={entry.slug}>
<span class="text-color-dim-1">{pathPrefixes.join("/")} / </span><span>{entry.collectionEntry.data.title}</span>
</DocLink>
) : (
<DocLink id={x.id}>{x.data.title}</DocLink>
<DocLink version={version} slug={entry.slug}>{entry.collectionEntry.data.title}</DocLink>
)
})}
</div>
59 changes: 54 additions & 5 deletions src/components/DocLinks.astro
Original file line number Diff line number Diff line change
@@ -1,13 +1,62 @@
---
import { getCollection } from "astro:content";
import { docsCollections, sectionAndSortEntries } from "../utils/docsCollections";
import { docsCollections, getDocumentation } from "../utils/docsCollections";
import DocLinkGroup from "./DocLinkGroup.astro";
import rawVersionList from "../content/docs/versions.json";

const docsEntries = await getCollection("docs");
const versionList = rawVersionList.toSorted((a, b) => a == "preview" ? -1 : b.localeCompare(a));

interface Props {
version: string;
}

const { version } = Astro.props;

const docsEntries = await getCollection("docs", x => !x.id.includes("/runtime/"));
---
<script>
const selects = document.querySelectorAll("#versionSelector");
selects.forEach((select) => {
const selectElm = select as HTMLSelectElement;
selectElm.onchange = async () => {
const routeParts = document.location.pathname.split("/");
const isVersioned = routeParts[2].startsWith("v") || routeParts[2] == "preview";
if (isVersioned) {
routeParts[2] = selectElm.value;
} else {
routeParts.splice(2, 0, selectElm.value);
}
const route = routeParts.join("/");
const url = new URL(route, document.location.origin);
const response = await fetch(url);
if (response.status == 200) {
document.location.pathname = routeParts.join("/");
} else {
document.location.pathname = `/docs/${selectElm.value}/intro`;
}
};
})
</script>

<h1 class="text-2xl lg:text-3xl font-semibold mb-4">Grain Docs</h1>

{docsCollections.map(coll => (
<DocLinkGroup title={coll.title} entries={sectionAndSortEntries(docsEntries.filter((x: any) => x.id.startsWith(coll.slugPrefix))).map(x => x.collectionEntry)} />
))}
<div class="w-full border-2 border-solid border-white rounded-lg">
<select
id="versionSelector"
name="versionSelector"
class="w-full p-2 bg-inherit border-solid border-r-4 border-transparent"
>
{versionList.map((currVersion) => {
return <option value={currVersion} selected={currVersion == version ? "selected" : ""}>{currVersion}</option>;
})}
</select>
</div>

{docsCollections(version).map((section) => {
const entries = getDocumentation(version, docsEntries).filter((entry) => entry.sectionTitle == section.title);
return <DocLinkGroup
version={version}
title={section.title}
entries={entries}
/>
})}
7 changes: 4 additions & 3 deletions src/components/DocsEntry.astro
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,14 @@ const sidebarClass = "fixed overflow-auto w-[calc(var(--container-width)/5)] xl:

interface Props {
data: { title?: string | undefined };
version: string;
section: string | null;
headings: MarkdownHeading[];
prev: CollectionEntry<"docs"> | undefined;
next: CollectionEntry<"docs"> | undefined;
}

const { data, section, headings, prev, next } = Astro.props;
const { data, version, section, headings, prev, next } = Astro.props;
---

<script>
Expand All @@ -46,13 +47,13 @@ const { data, section, headings, prev, next } = Astro.props;
</div>

<div id="sidebar-docs-links" class="lg:hidden z-30 w-72 h-[calc(100vh-6rem)] fixed overflow-scroll top-24 left-0 p-3 bg-color-background shadow transition-transform -translate-x-full">
<DocLinks />
<DocLinks version={version} />
</div>

<div class="container mx-auto pt-5 md:pt-14 px-4 lg:px-0">
<div class="flex justify-end">
<div class=`${sidebarClass} hidden lg:block left-[calc(50%-calc(var(--container-width)/2))] pl-5`>
<DocLinks />
<DocLinks version={version} />
</div>

<article class="w-full lg:w-4/5 xl:w-2/3 xl:float-none xl:mx-auto lg:pl-14 xl:pr-14">
Expand Down
3 changes: 1 addition & 2 deletions src/content.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ const docsCollection = defineCollection({
});

export const collections = {
// TODO(#390): Support doc versioning
"blog": blogCollection,
"docs": docsCollection,
"docs": docsCollection
};
Loading