Skip to content

Commit af8c958

Browse files
authored
Revert "Revert "Test TS compilation of all docs (#264)" (#265)" (#266)
This reverts commit d90f50c.
1 parent d90f50c commit af8c958

1 file changed

Lines changed: 140 additions & 3 deletions

File tree

packages/integration-tests/src/readme.test.ts

Lines changed: 140 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
/**
2-
* Tests that code examples in README.md files are valid TypeScript.
2+
* Tests that code examples in README.md and docs MDX files are valid TypeScript.
33
*
44
* This ensures documentation stays in sync with the actual API.
55
*
66
* - Main README: Full type-checking (examples should be complete)
77
* - Package READMEs: Syntax-only checking (examples are intentionally minimal)
8+
* - Docs MDX files: Syntax-only checking (examples reference external packages)
89
*/
910

1011
import { execSync } from "node:child_process";
@@ -17,19 +18,21 @@ import {
1718
writeFileSync,
1819
} from "node:fs";
1920
import { tmpdir } from "node:os";
20-
import { basename, join } from "node:path";
21+
import { basename, join, relative } from "node:path";
2122
import { describe, expect, it } from "vitest";
2223

2324
const IMPORT_PACKAGE_REGEX = /from ["']([^"']+)["']/;
2425
const REPO_ROOT = join(import.meta.dirname, "../../..");
2526
const PACKAGES_DIR = join(REPO_ROOT, "packages");
27+
const DOCS_CONTENT_DIR = join(REPO_ROOT, "apps/docs/content");
2628

2729
/**
2830
* Extract TypeScript code blocks from markdown content.
31+
* Handles optional MDX metadata after the language tag (e.g., `title="..." lineNumbers`).
2932
*/
3033
function extractTypeScriptBlocks(markdown: string): string[] {
3134
const blocks: string[] = [];
32-
const regex = /```(?:typescript|ts)\n([\s\S]*?)```/g;
35+
const regex = /```(?:typescript|tsx?)(?:[^\S\n][^\n]*)?\n([\s\S]*?)```/g;
3336
let match = regex.exec(markdown);
3437

3538
while (match !== null) {
@@ -40,6 +43,26 @@ function extractTypeScriptBlocks(markdown: string): string[] {
4043
return blocks;
4144
}
4245

46+
/**
47+
* Extract TypeScript and TSX code blocks from MDX content.
48+
* Returns blocks tagged with their language for appropriate validation.
49+
*/
50+
function extractCodeBlocks(
51+
markdown: string
52+
): Array<{ code: string; lang: "ts" | "tsx" }> {
53+
const blocks: Array<{ code: string; lang: "ts" | "tsx" }> = [];
54+
const regex = /```(typescript|ts|tsx)(?:[^\S\n][^\n]*)?\n([\s\S]*?)```/g;
55+
let match = regex.exec(markdown);
56+
57+
while (match !== null) {
58+
const lang = match[1] === "tsx" ? "tsx" : "ts";
59+
blocks.push({ code: match[2].trim(), lang });
60+
match = regex.exec(markdown);
61+
}
62+
63+
return blocks;
64+
}
65+
4366
/**
4467
* Create a temporary directory with proper tsconfig and package setup
4568
* to type-check the code blocks.
@@ -181,6 +204,84 @@ function findPackageReadmes(): Array<{ path: string; name: string }> {
181204
return readmes;
182205
}
183206

207+
/**
208+
* Recursively find all MDX files in the docs content directory.
209+
*/
210+
function findDocsMdxFiles(dir: string): Array<{ path: string; name: string }> {
211+
const files: Array<{ path: string; name: string }> = [];
212+
213+
if (!existsSync(dir)) {
214+
return files;
215+
}
216+
217+
const entries = readdirSync(dir, { withFileTypes: true });
218+
for (const entry of entries) {
219+
const fullPath = join(dir, entry.name);
220+
if (entry.isDirectory()) {
221+
files.push(...findDocsMdxFiles(fullPath));
222+
} else if (entry.name.endsWith(".mdx") || entry.name.endsWith(".md")) {
223+
files.push({
224+
path: fullPath,
225+
name: relative(REPO_ROOT, fullPath),
226+
});
227+
}
228+
}
229+
230+
return files;
231+
}
232+
233+
/**
234+
* Valid packages that can appear in import statements across all docs.
235+
* Superset of the README valid packages — includes external dependencies
236+
* referenced in guides and examples.
237+
*/
238+
const VALID_DOC_PACKAGES = [
239+
// Chat SDK packages
240+
"chat",
241+
"@chat-adapter/slack",
242+
"@chat-adapter/teams",
243+
"@chat-adapter/gchat",
244+
"@chat-adapter/discord",
245+
"@chat-adapter/telegram",
246+
"@chat-adapter/github",
247+
"@chat-adapter/linear",
248+
"@chat-adapter/whatsapp",
249+
"@chat-adapter/state-redis",
250+
"@chat-adapter/state-ioredis",
251+
"@chat-adapter/state-pg",
252+
"@chat-adapter/state-memory",
253+
"@chat-adapter/shared",
254+
// Frameworks and runtimes
255+
"next/server",
256+
"next",
257+
"hono",
258+
// AI SDK
259+
"ai",
260+
"@ai-sdk/anthropic",
261+
"@ai-sdk/openai",
262+
"@ai-sdk/gateway",
263+
// Vercel packages
264+
"@vercel/sandbox",
265+
"@vercel/functions",
266+
"workflow",
267+
"workflow/next",
268+
"workflow/api",
269+
// Database and state
270+
"redis",
271+
"ioredis",
272+
"pg",
273+
"postgres",
274+
// Build and test tooling
275+
"tsup",
276+
"vitest",
277+
"vitest/config",
278+
// External libraries used in guides
279+
"bash-tool",
280+
"@octokit/rest",
281+
// Hypothetical example package used in contributing docs
282+
"chat-adapter-matrix",
283+
];
284+
184285
describe("Main README.md code examples", () => {
185286
const mainReadmePath = join(REPO_ROOT, "README.md");
186287

@@ -299,3 +400,39 @@ describe("Package README code examples", () => {
299400
});
300401
}
301402
});
403+
404+
describe("Docs MDX code examples", () => {
405+
const docFiles = findDocsMdxFiles(DOCS_CONTENT_DIR);
406+
407+
for (const { path: filePath, name: fileName } of docFiles) {
408+
it(`${fileName} should have valid syntax in code blocks`, () => {
409+
const content = readFileSync(filePath, "utf-8");
410+
const codeBlocks = extractCodeBlocks(content);
411+
412+
// Skip files without code blocks
413+
if (codeBlocks.length === 0) {
414+
return;
415+
}
416+
417+
for (const { code: block, lang } of codeBlocks) {
418+
// Skip brace/paren balance checks for docs — they intentionally use
419+
// partial snippets (e.g., showing just an option without the opening brace).
420+
// Import validation is the most valuable check for keeping docs in sync.
421+
422+
// Check that imports reference valid packages
423+
const importMatches = block.match(/from ["']([^"']+)["']/g) || [];
424+
for (const importMatch of importMatches) {
425+
const pkg = importMatch.match(IMPORT_PACKAGE_REGEX)?.[1];
426+
if (pkg && !pkg.startsWith(".") && !pkg.startsWith("@/")) {
427+
const isValid =
428+
VALID_DOC_PACKAGES.includes(pkg) || pkg.startsWith("node:");
429+
expect(
430+
isValid,
431+
`${fileName}: Unknown import "${pkg}" in ${lang} code block`
432+
).toBe(true);
433+
}
434+
}
435+
}
436+
});
437+
}
438+
});

0 commit comments

Comments
 (0)