Something went wrong. Try again.
Training wheels for the terminal
Something went wrong. Try again.
5.5 kB · 149 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150// Every doc must compile as MDX (a stray `{` in prose compiles but breaks the// page at runtime), every embedded <RecipeBuilder> must point at a real recipe,// and every link to another doc must resolve. Docs live in chapter folders.import { describe, it, expect } from "vitest";import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";import { join } from "node:path";import { evaluate } from "@mdx-js/mdx";import * as runtime from "react/jsx-runtime";import { createElement } from "react";import { renderToStaticMarkup } from "react-dom/server";import remarkFrontmatter from "remark-frontmatter";import fm from "front-matter";import rehypeShellPlugin from "./rehypeShellPlugin";import { Recipe } from "../features/CommandBuilder/commandBuilderTypes";import { tokensInUse } from "../features/CommandBuilder/commandAssembly";
const DATA_DIR = join(__dirname, "../../content");const DOCS_DIR = join(DATA_DIR, "docs");const chapters = readdirSync(DOCS_DIR).filter((entry) => statSync(join(DOCS_DIR, entry)).isDirectory(),);const docs = chapters.flatMap((chapter) => readdirSync(join(DOCS_DIR, chapter)) .filter((f) => f.endsWith(".mdx")) .map((f) => ({ file: `${chapter}/${f}`, text: readFileSync(join(DOCS_DIR, chapter, f), "utf8"), })),);
describe("docs layout", () => { it("keeps every doc in a chapter folder (the app ignores the rest)", () => { const loose = readdirSync(DOCS_DIR).filter((f) => f.endsWith(".mdx")); expect(loose, "move these into a chapter folder").toEqual([]); });
it.each(chapters)("titles chapter %s in its chapter.json", (chapter) => { const path = join(DOCS_DIR, chapter, "chapter.json"); expect(existsSync(path), `${chapter}/chapter.json not found`).toBe(true); const { title } = JSON.parse(readFileSync(path, "utf8")); expect(typeof title === "string" && title !== "", "needs a title").toBe( true, ); });});
/** Props of every <RecipeBuilder …/> tag in a doc. */function recipeBuilders(text: string): Record<string, string>[] { return [...text.matchAll(/<RecipeBuilder\b([^>]*?)\/>/gs)].map((m) => Object.fromEntries( [...m[1].matchAll(/(\w+)="([^"]*)"/g)].map((a) => [a[1], a[2]]), ), );}
// Stand-ins for the app's MDX components, which need the terminal and store.const Stub = ({ children }: { children?: React.ReactNode }) => createElement("div", null, children);const components = { CodeBlock: Stub, InstallBlock: Stub, PromptDisplay: Stub, RecipeBuilder: Stub, Link: Stub,};
/** Paths (relative to content/docs) of every /docs/ link in a doc. */function docLinks(text: string): string[] { return [...text.matchAll(/\]\(\/docs\/([^)#\s]+)/g)].map((m) => m[1]);}
// A doc's platform, from its filename: "x.windows.mdx" -> "windows".// "unix" docs serve macOS and Linux, so they may link to "mac" docs.function platformOf(path: string): string | undefined { return path.match(/\.(windows|unix|mac)\.mdx$/)?.[1];}const COMPATIBLE: Record<string, string[]> = { windows: ["windows"], unix: ["unix", "mac"], mac: ["unix", "mac"],};
// The basics are a sequence: each tutorial should end by pointing somewhere.const SEQUENCE_CHAPTER = "1-cli-basics/";
describe.each(docs)("doc $file", ({ file, text }) => { it("renders as MDX", async () => { // Rendering, not just compiling: prose like `{{duration}}` compiles fine // (it's an object literal) and only fails when evaluated. const { body } = fm(text); const { default: Content } = await evaluate(body, { ...runtime, remarkPlugins: [[remarkFrontmatter, ["yaml", "toml"]]], rehypePlugins: [rehypeShellPlugin], useMDXComponents: () => components, }); expect(renderToStaticMarkup(createElement(Content))).not.toBe(""); });
it("links only to docs that exist", () => { for (const link of docLinks(text)) { expect(existsSync(join(DOCS_DIR, link)), `link to /docs/${link}`).toBe( true, ); } });
it("links only to docs for the same platform", () => { const own = platformOf(file); if (!own) return; // platform-neutral docs may link anywhere for (const link of docLinks(text)) { const target = platformOf(link); if (!target) continue; expect( COMPATIBLE[own], `${file} (${own}) links to /docs/${link} (${target})`, ).toContain(target); } });
it.skipIf(!file.startsWith(SEQUENCE_CHAPTER))( "ends by linking to a next step", () => { // The last paragraph should contain a link to another doc. const lastParagraph = text.trimEnd().split(/\n\s*\n/).pop() ?? ""; const next = docLinks(lastParagraph).filter((l) => !file.endsWith(l)); expect(next, "add a 'Next up' link at the end").not.toEqual([]); }, );
it("embeds only recipes that exist, capturing into tokens they use", () => { const builders = recipeBuilders(text); const pageTokens = new Set<string>(); for (const { src } of builders) { const path = join(DATA_DIR, "recipes", src); expect(existsSync(path), `recipe ${src} not found`).toBe(true); const recipe = JSON.parse(readFileSync(path, "utf8")) as Recipe; tokensInUse(recipe).forEach((t) => pageTokens.add(t)); } for (const { captureAs } of builders) { if (captureAs) { expect( [...pageTokens], `captureAs="${captureAs}" isn't used by any recipe on the page`, ).toContain(captureAs); } } });});