diff --git a/.github/workflows/sync-version-docs.yml b/.github/workflows/sync-version-docs.yml index 71b22d94f7..2957b0c397 100644 --- a/.github/workflows/sync-version-docs.yml +++ b/.github/workflows/sync-version-docs.yml @@ -8,6 +8,7 @@ on: - completed branches: # Match editBranch values with a pinned commitSha in `docs/lib/docs-version-sources.json`. + - next - "v*.*.x" permissions: diff --git a/docs/app/docs/layout.tsx b/docs/app/docs/layout.tsx index ee68556c0b..a72c78227f 100644 --- a/docs/app/docs/layout.tsx +++ b/docs/app/docs/layout.tsx @@ -10,11 +10,12 @@ import type { PageTreesByVersion } from "./provider"; import { DocsProvider } from "./provider"; import { DocsShell } from "./shell"; -const [latestVersion, version16] = docsVersions; +const [betaVersion, latestVersion, version16] = docsVersions; const pageTreesByVersion = { latest: getSourceFor(latestVersion.id).getPageTree(), "1.6": getSourceFor(version16.id).getPageTree(), + beta: getSourceFor(betaVersion.id).getPageTree(), } satisfies PageTreesByVersion; function getAvailablePaths(version: DocsVersion) { @@ -27,6 +28,7 @@ function getAvailablePaths(version: DocsVersion) { const versionAvailability = { latest: getAvailablePaths(latestVersion), "1.6": getAvailablePaths(version16), + beta: getAvailablePaths(betaVersion), } satisfies VersionAvailability; const resolvedDocsVersions = loadDocsVersions(); export default function Layout({ children }: { children: ReactNode }) { diff --git a/docs/components/version-switcher.tsx b/docs/components/version-switcher.tsx index ce4ba5fe23..27cad478c8 100644 --- a/docs/components/version-switcher.tsx +++ b/docs/components/version-switcher.tsx @@ -49,6 +49,7 @@ function useDismissablePopover( function getVersionTitle(version: DocsVersion) { if (version.id === "latest") return "Latest Version"; + if (version.id === "beta") return "Beta Version"; return `Version ${version.id}`; } diff --git a/docs/content/_generated/docs/beta/.gitkeep b/docs/content/_generated/docs/beta/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/content/_generated/docs/latest/.gitkeep b/docs/content/_generated/docs/latest/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docs/lib/docs-version-sources.json b/docs/lib/docs-version-sources.json index 35f1a68769..141f4509e6 100644 --- a/docs/lib/docs-version-sources.json +++ b/docs/lib/docs-version-sources.json @@ -1,6 +1,6 @@ { "latest": { - "contentDirectory": "docs", + "contentDirectory": "_generated/docs/latest", "editBranch": "main", "commitSha": null }, @@ -8,5 +8,10 @@ "contentDirectory": "_generated/docs/v1-6", "editBranch": "v1.6.x", "commitSha": "c11bfa667f9f260092c3d4c3798fe50d74b79718" + }, + "beta": { + "contentDirectory": "_generated/docs/beta", + "editBranch": "next", + "commitSha": "be781d5fd26aa4719f5f4fa3ebd767736d4883b1" } } diff --git a/docs/lib/docs-versions.test.ts b/docs/lib/docs-versions.test.ts index a015bfcfaf..2c2989f5b0 100644 --- a/docs/lib/docs-versions.test.ts +++ b/docs/lib/docs-versions.test.ts @@ -6,17 +6,29 @@ import { getVersionFromPathname, getVersionTargetHref, resolveVersionFromSlug, + scopeDocsHref, versionedDocsHref, } from "./docs-versions"; -const [latestVersion, version16] = docsVersions; +const [betaVersion, latestVersion, version16] = docsVersions; const availability: VersionAvailability = { latest: [], "1.6": ["/docs/introduction", "/docs/plugins/scim"], + beta: ["/docs/introduction", "/docs/plugins/scim"], }; describe("documentation versions", () => { + it("lists beta before stable while keeping stable as the default", () => { + expect(docsVersions.map((version) => version.id)).toEqual([ + "beta", + "latest", + "1.6", + ]); + expect(getVersionFromPathname("/docs/introduction").id).toBe("latest"); + expect(resolveVersionFromSlug([]).version.id).toBe("latest"); + }); + it("targets maintenance branches for documentation edits", () => { expect(docsVersionSources[version16.id].editBranch).toBe("v1.6.x"); }); @@ -28,6 +40,51 @@ describe("documentation versions", () => { ); }); + it("pins beta documentation to next and keeps its release line separate", () => { + expect(docsVersionSources[betaVersion.id].editBranch).toBe("next"); + expect(docsVersionSources[betaVersion.id].commitSha).toMatch( + /^[0-9a-f]{40}$/, + ); + expect(betaVersion.releaseLine).toBe("1.8"); + }); + + it("resolves beta pages and scopes their links", () => { + expect(resolveVersionFromSlug(["beta", "plugins", "scim"])).toEqual({ + version: betaVersion, + relSlug: ["plugins", "scim"], + }); + expect(getVersionFromPathname("/docs/beta/plugins/scim")).toBe(betaVersion); + expect(getVersionFromPathname("/docs/beta-tutorial")).toBe(latestVersion); + expect(scopeDocsHref("/docs/plugins/scim", betaVersion)).toBe( + "/docs/beta/plugins/scim", + ); + expect(scopeDocsHref("/docs/beta/plugins/scim", betaVersion)).toBe( + "/docs/beta/plugins/scim", + ); + expect(scopeDocsHref("/docs/1.6/introduction", betaVersion)).toBe( + "/docs/1.6/introduction", + ); + }); + + it("switches between beta and stable without losing the current page", () => { + expect( + getVersionTargetHref( + "/docs/beta/plugins/scim", + betaVersion, + latestVersion, + { ...availability, latest: ["/docs/plugins/scim"] }, + ), + ).toBe("/docs/plugins/scim"); + expect( + getVersionTargetHref( + "/docs/plugins/scim", + latestVersion, + betaVersion, + availability, + ), + ).toBe("/docs/beta/plugins/scim"); + }); + it("resolves archived version slugs", () => { expect(resolveVersionFromSlug(["1.6", "plugins", "scim"])).toEqual({ version: version16, diff --git a/docs/lib/docs-versions.ts b/docs/lib/docs-versions.ts index ea33d511e8..f51714e527 100644 --- a/docs/lib/docs-versions.ts +++ b/docs/lib/docs-versions.ts @@ -18,6 +18,12 @@ export interface DocsVersionConfig { } export const docsVersions = [ + { + label: "v1.8 (Beta)", + releaseLine: "1.8", + id: "beta", + badge: null, + }, { label: "v1.7 (Latest)", releaseLine: "1.7", @@ -44,7 +50,7 @@ const docsRootPathPattern = /^\/docs\/?([?#].*)?$/; /** * The default (latest) version entry. */ -export const latestVersion = docsVersions[0]; +export const latestVersion = docsVersions[1]; /** * Find a version config by its URL slug. diff --git a/docs/lib/llm-text.test.ts b/docs/lib/llm-text.test.ts index 374f8b43c2..bb796d236d 100644 --- a/docs/lib/llm-text.test.ts +++ b/docs/lib/llm-text.test.ts @@ -29,6 +29,7 @@ describe("LLM routes", () => { expect(content).toContain("https://better-auth.com/docs/llms.txt"); expect(content).toContain("https://better-auth.com/docs/1.6/llms.txt"); + expect(content).toContain("https://better-auth.com/docs/beta/llms.txt"); expect(content).toContain("https://mcp.better-auth.com/mcp"); expect(content).not.toContain("/llms.txt/docs/"); }); @@ -61,6 +62,9 @@ describe("LLM routes", () => { "/docs/1.6/plugins/scim.md", ); expect(getLegacyMarkdownTarget(["1.6"])).toBe("/docs/1.6/llms.txt"); + expect(getLegacyMarkdownTarget(["beta", "plugins", "scim"])).toBe( + "/docs/beta/plugins/scim.md", + ); expect(getLegacyMarkdownTarget(["docs", "introduction.txt"])).toBeNull(); }); diff --git a/docs/lib/markdown-links.test.ts b/docs/lib/markdown-links.test.ts index 1de5f20f44..eba9e1e439 100644 --- a/docs/lib/markdown-links.test.ts +++ b/docs/lib/markdown-links.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest"; import { docsVersions } from "./docs-versions"; import { scopeMarkdownLinks } from "./markdown-links"; -const version16 = docsVersions[1]; +const version16 = docsVersions[2]; describe("versioned Markdown links", () => { it("scopes unversioned links without duplicating version prefixes", () => { diff --git a/docs/lib/source.ts b/docs/lib/source.ts index 7bb6e10853..41729da4db 100644 --- a/docs/lib/source.ts +++ b/docs/lib/source.ts @@ -1,7 +1,7 @@ import { loader } from "fumadocs-core/source"; import { lucideIconsPlugin } from "fumadocs-core/source/lucide-icons"; import { toFumadocsSource } from "fumadocs-mdx/runtime/server"; -import { blogCollection, docs, docsV16 } from "@/.source/server"; +import { blogCollection, docs, docsBeta, docsV16 } from "@/.source/server"; import type { DocsVersionId } from "./docs-versions"; import { pageTreePlugin } from "./page-tree"; @@ -21,9 +21,17 @@ export const sourceV16 = loader({ plugins: docsPlugins, }); +export const sourceBeta = loader({ + baseUrl: "/docs/beta", + source: docsBeta.toFumadocsSource(), + pageTree: { noRef: true }, + plugins: docsPlugins, +}); + const docsSources = { latest: source, "1.6": sourceV16, + beta: sourceBeta, } satisfies Record; export function getSourceFor(versionId: DocsVersionId) { diff --git a/docs/scripts/sync-versions.ts b/docs/scripts/sync-versions.ts index cf996df2bd..e3e3faa8b1 100644 --- a/docs/scripts/sync-versions.ts +++ b/docs/scripts/sync-versions.ts @@ -24,10 +24,21 @@ const releaseVersionsPath = join( "docs", "release-versions.json", ); +const localReleaseVersion = await readPackageVersion( + join(repositoryRoot, packagePath), +); +const localVersion = docsVersions.find((version) => + localReleaseVersion.startsWith(`${version.releaseLine}.`), +); +if (!localVersion) { + throw new Error( + `No documentation version configured for ${localReleaseVersion}`, + ); +} const versionsToSync = docsVersions.flatMap((version) => { const source = docsVersionSources[version.id]; - if (source.commitSha === null) return []; - return [{ ...version, ...source, commitSha: source.commitSha }]; + if (version.id === localVersion.id) return []; + return [{ ...version, ...source }]; }); function git(args: string[], cwd?: string) { @@ -57,7 +68,7 @@ async function readPackageVersion(path: string) { } async function prepareWorktree(version: (typeof versionsToSync)[number]) { - const { commitSha } = version; + const revision = `refs/docs-versions/${version.id.replaceAll(".", "-")}`; const checkoutDirectory = resolve( tempRoot, "worktrees", @@ -71,7 +82,7 @@ async function prepareWorktree(version: (typeof versionsToSync)[number]) { "--detach", "--no-checkout", checkoutDirectory, - commitSha, + revision, ]); await git(["-C", checkoutDirectory, "sparse-checkout", "init", "--cone"]); await git([ @@ -88,19 +99,22 @@ async function prepareWorktree(version: (typeof versionsToSync)[number]) { "checkout", "--quiet", "--detach", - commitSha, + revision, ]); - return { checkoutDirectory, commitSha, version }; + return { checkoutDirectory, version }; } async function syncVersion({ checkoutDirectory, - commitSha, version, }: Awaited>) { const destination = join(docsRoot, "content", version.contentDirectory); + const sourceRevision = + checkoutDirectory === repositoryRoot + ? "local" + : (version.commitSha?.slice(0, 9) ?? "HEAD"); console.log( - `[sync-versions] ${version.editBranch}@${commitSha.slice(0, 9)}:${remotePath} → ${destination}`, + `[sync-versions] ${version.editBranch}@${sourceRevision}:${remotePath} → ${destination}`, ); const sourcePath = join(checkoutDirectory, remotePath); try { @@ -112,12 +126,17 @@ async function syncVersion({ ); } - await rm(destination, { recursive: true, force: true }); - await cp(sourcePath, destination, { recursive: true }); - await writeFile(join(destination, ".gitkeep"), ""); const releaseVersion = await readPackageVersion( join(checkoutDirectory, packagePath), ); + if (!releaseVersion.startsWith(`${version.releaseLine}.`)) { + throw new Error( + `Invalid release version for ${version.id}: expected ${version.releaseLine}.x, received ${releaseVersion}`, + ); + } + await rm(destination, { recursive: true, force: true }); + await cp(sourcePath, destination, { recursive: true }); + await writeFile(join(destination, ".gitkeep"), ""); return [version.id, releaseVersion] as const; } @@ -137,11 +156,16 @@ try { "origin", ...versionsToSync.map( (version) => - `+${version.commitSha}:refs/docs-versions/${version.id.replaceAll(".", "-")}`, + `+${version.commitSha ?? version.editBranch}:refs/docs-versions/${version.id.replaceAll(".", "-")}`, ), ]); - const worktrees = []; + const worktrees = [ + { + checkoutDirectory: repositoryRoot, + version: { ...localVersion, ...docsVersionSources[localVersion.id] }, + }, + ]; for (const version of versionsToSync) { worktrees.push(await prepareWorktree(version)); } @@ -155,17 +179,11 @@ try { "One or more documentation versions failed to sync", ); } - const latestVersion = docsVersions.find((version) => version.id === "latest"); - if (!latestVersion) throw new Error("Missing latest documentation version"); - const releaseVersions = Object.fromEntries([ - [ - latestVersion.id, - await readPackageVersion(join(repositoryRoot, packagePath)), - ], - ...results.flatMap((result) => + const releaseVersions = Object.fromEntries( + results.flatMap((result) => result.status === "fulfilled" ? [result.value] : [], ), - ]); + ); await writeFile( releaseVersionsPath, `${JSON.stringify(releaseVersions, null, 2)}\n`, diff --git a/docs/source.config.ts b/docs/source.config.ts index 2caf6751e4..c74bc77e86 100644 --- a/docs/source.config.ts +++ b/docs/source.config.ts @@ -12,6 +12,12 @@ import { remarkAutoTypeTable, } from "fumadocs-typescript"; import * as z from "zod"; +import packageMetadata from "../packages/better-auth/package.json" with { + type: "json", +}; +import { docsVersionSources } from "./lib/docs-version-sources"; +import type { DocsVersion } from "./lib/docs-versions"; +import { docsVersions } from "./lib/docs-versions"; const docsPageSchema = pageSchema.extend({ sidebarBadge: z.string().min(1).optional(), @@ -22,8 +28,16 @@ const processedMarkdownOptions = { mdxAsPlaceholder: ["APIMethod"], } satisfies LLMsOptions; +function getContentDirectory(version: DocsVersion) { + return packageMetadata.version.startsWith(`${version.releaseLine}.`) + ? "./content/docs" + : `./content/${docsVersionSources[version.id].contentDirectory}`; +} + +const [betaVersion, latestVersion, version16] = docsVersions; + export const docs = defineDocs({ - dir: "./content/docs", + dir: getContentDirectory(latestVersion), docs: { schema: docsPageSchema, postprocess: { @@ -34,7 +48,18 @@ export const docs = defineDocs({ }); export const docsV16 = defineDocs({ - dir: "./content/_generated/docs/v1-6", + dir: getContentDirectory(version16), + docs: { + schema: docsPageSchema, + postprocess: { + includeProcessedMarkdown: processedMarkdownOptions, + }, + async: true, + }, +}); + +export const docsBeta = defineDocs({ + dir: getContentDirectory(betaVersion), docs: { schema: docsPageSchema, postprocess: {