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
1011import { execSync } from "node:child_process" ;
@@ -17,19 +18,21 @@ import {
1718 writeFileSync ,
1819} from "node:fs" ;
1920import { tmpdir } from "node:os" ;
20- import { basename , join } from "node:path" ;
21+ import { basename , join , relative } from "node:path" ;
2122import { describe , expect , it } from "vitest" ;
2223
2324const IMPORT_PACKAGE_REGEX = / f r o m [ " ' ] ( [ ^ " ' ] + ) [ " ' ] / ;
2425const REPO_ROOT = join ( import . meta. dirname , "../../.." ) ;
2526const 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 */
3033function extractTypeScriptBlocks ( markdown : string ) : string [ ] {
3134 const blocks : string [ ] = [ ] ;
32- const regex = / ` ` ` (?: t y p e s c r i p t | t s ) \n ( [ \s \S ] * ?) ` ` ` / g;
35+ const regex = / ` ` ` (?: t y p e s c r i p t | t s x ? ) (?: [ ^ \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 = / ` ` ` ( t y p e s c r i p t | t s | t s x ) (?: [ ^ \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+
184285describe ( "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 ( / f r o m [ " ' ] ( [ ^ " ' ] + ) [ " ' ] / 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