Skip to content

Instantly share code, notes, and snippets.

@NathanFlurry
Created January 22, 2026 21:17
Show Gist options
  • Select an option

  • Save NathanFlurry/df6b6924d878b6a8e266994c9a4c4e15 to your computer and use it in GitHub Desktop.

Select an option

Save NathanFlurry/df6b6924d878b6a8e266994c9a4c4e15 to your computer and use it in GitHub Desktop.

Plan: Automatic Type Checking for Documentation Code Blocks

Overview

Implement automatic TypeScript type checking for code examples in MDX documentation during Astro build and dev. This catches type errors in docs before they go live.

Architecture

Approach: Astro Integration that runs at astro:config:setup (before build/dev)

Flow:

  1. Scan all MDX files in src/content/docs/
  2. Extract TypeScript/TS code blocks with line numbers
  3. Filter out blocks marked with @nocheck
  4. Write each to staging project's snippets/ directory
  5. Run single tsc --noEmit on all snippets
  6. Map errors back to source MDX files
  7. Fail build/dev if any type errors exist

Code Block Annotation Format (New Spec)

Replace the current mdxAnnotations JSON format with a simple space-separated syntax:

Format:

```{lang}
```{lang} {title}
```{lang} @nocheck
```{lang} {title} @nocheck

Rules:

  1. First token is always the language (required)
  2. @nocheck flag can appear anywhere after the language
  3. Any other token is treated as the title
  4. Only one title allowed (no spaces in titles, use kebab-case)

Examples:

Code Fence Language Title Nocheck
```ts ts - false
```ts server.ts ts server.ts false
```ts @nocheck ts - true
```ts server.ts @nocheck ts server.ts true
```bash install.sh bash install.sh false

File Structure

website/
├── scripts/
│   └── typecheck-staging/           # NEW
│       ├── package.json             # rivetkit + typescript deps
│       ├── tsconfig.json            # Strict TS config
│       ├── ambient.d.ts             # Declares common doc patterns
│       └── snippets/                # Generated at build (gitignored)
├── src/
│   └── integrations/
│       └── typecheck-code-blocks.ts # NEW: Astro integration

Implementation Steps

1. Create Staging Project

website/scripts/typecheck-staging/package.json

{
  "name": "docs-typecheck-staging",
  "private": true,
  "type": "module",
  "dependencies": {
    "rivetkit": "workspace:*",
    "hono": "^4.7.0",
    "zod": "^4.1.0"
  },
  "devDependencies": {
    "typescript": "^5.7.3",
    "@types/node": "^22.13.1"
  }
}

website/scripts/typecheck-staging/tsconfig.json

{
  "compilerOptions": {
    "target": "ESNext",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "skipLibCheck": true,
    "noEmit": true,
    "esModuleInterop": true,
    "lib": ["ESNext", "DOM"],
    "types": ["node"]
  },
  "include": ["snippets/**/*.ts", "ambient.d.ts"]
}

website/scripts/typecheck-staging/ambient.d.ts

// Ambient declarations for common doc patterns
declare module "./registry" {
  export const registry: import("rivetkit").Registry<any>;
}
declare module "./actors" {
  export const registry: import("rivetkit").Registry<any>;
}

2. Create Astro Integration

website/src/integrations/typecheck-code-blocks.ts

Key functions:

  • extractCodeBlocks(content, sourceFile) - Parse MDX for TS code blocks, detect @nocheck
  • wrapCodeForTypecheck(code) - Add common imports, wrap partial snippets
  • generateSnippetFilename(sourceFile, lineNumber) - Create unique filenames
  • parseTscOutput(output, mapping) - Map tsc errors to source files

The integration:

  1. Runs during both build and dev (at astro:config:setup)
  2. Cleans/creates snippets/ directory
  3. Extracts all TS code blocks from MDX files
  4. Filters out blocks with @nocheck annotation
  5. Wraps code and writes to staging project
  6. Runs npx tsc --noEmit
  7. Reports errors with source file + line number
  8. Throws to fail build/dev on errors

3. Update Rehype Plugin for New Annotation Format

Modify website/src/mdx/rehype.ts:

Update rehypeParseCodeBlocks() to parse the new format:

function rehypeParseCodeBlocks() {
  return (tree) => {
    visit(tree, "element", (node, _nodeIndex, parentNode) => {
      if (node.tagName === "code") {
        // Parse language from className
        if (node.properties.className) {
          parentNode.properties.language =
            node.properties.className[0]?.replace(/^language-/, "");
        }

        // Parse annotation format: {title}? @nocheck?
        // Language is already parsed from className above
        const info = parentNode.properties?.annotation || node.data;
        if (info && typeof info === "string") {
          const tokens = info.trim().split(/\s+/);

          for (const token of tokens) {
            if (token === "@nocheck") {
              parentNode.properties.nocheck = true;
            } else if (token && !token.startsWith("@")) {
              // Non-flag token is the title
              parentNode.properties.title = token;
            }
          }
        }
      }
    });
  };
}

Also remove from website/src/mdx/rehype.ts:

  • Remove transformerTemplateVariables import
  • Remove autofill transformer logic from rehypeShiki()

Also remove these files:

  • website/src/mdx/transformers.ts (template variables transformer)
  • website/src/components/v2/AutofillCodeBlock.tsx

Update website/src/components/v2/Code.tsx:

  • Remove autofill prop handling
  • Remove AutofillCodeBlock wrapper

4. Register Integration

Modify website/astro.config.mjs:

import { typecheckCodeBlocks } from './src/integrations/typecheck-code-blocks';

export default defineConfig({
  integrations: [
    typecheckCodeBlocks(),  // First - runs during config setup
    generateRoutes(),
    mdx({ /* ... */ }),
    // ... rest
  ],
});

5. Add postinstall Hook

Modify website/package.json:

{
  "scripts": {
    "postinstall": "cd scripts/typecheck-staging && pnpm install"
  }
}

6. Update .gitignore

scripts/typecheck-staging/snippets/
scripts/typecheck-staging/node_modules/

Edge Cases

Case Solution
Partial code snippets Auto-wrap in async IIFE, prepend common imports
Relative imports (./registry) Declare in ambient.d.ts
Intentional non-compiling code Add @nocheck to code fence
JavaScript blocks Only check typescript and ts language tags

Opt-Out Annotation

Authors can skip type checking by adding @nocheck to the code fence:

```ts @nocheck
// This won't be type checked
const incomplete =
```

Or with a title:

```ts broken-example.ts @nocheck
// This won't be type checked
const incomplete =
```

Error Output Example

[typecheck-code-blocks] Type errors found in documentation code blocks:
  /website/src/content/docs/actors/state.mdx:46
    Property 'foo' does not exist on type '{ count: number; }'.

Files to Create/Modify

File Action
website/scripts/typecheck-staging/package.json Create
website/scripts/typecheck-staging/tsconfig.json Create
website/scripts/typecheck-staging/ambient.d.ts Create
website/src/integrations/typecheck-code-blocks.ts Create
website/src/mdx/rehype.ts Modify - new annotation parsing, remove autofill
website/src/mdx/transformers.ts Delete
website/src/components/v2/AutofillCodeBlock.tsx Delete
website/src/components/v2/Code.tsx Modify - remove autofill
website/astro.config.mjs Modify - add integration
website/package.json Modify - add postinstall
website/.gitignore Modify - add staging dirs

Verification

  1. Run pnpm install in website/ (triggers staging setup)
  2. Run pnpm build - should pass with current docs
  3. Introduce a type error in a code block in actors/state.mdx
  4. Run pnpm build - should fail with clear error message
  5. Add @nocheck to the code fence - should pass
  6. Remove the intentional error and verify clean build
  7. Run pnpm dev - verify type checking also runs in dev mode
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment