Skip to content

Commit

Permalink
enable type-checked code blocks (#106)
Browse files Browse the repository at this point in the history
  • Loading branch information
souporserious committed Apr 30, 2024
1 parent 7726268 commit 469b021
Show file tree
Hide file tree
Showing 11 changed files with 121 additions and 28 deletions.
21 changes: 21 additions & 0 deletions .changeset/curly-mugs-count.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
'mdxts': minor
---

Enables type-checking for the `CodeBlock` component. To opt-out of type-checking, use the `allowErrors` prop on the code block:

```tsx allowErrors
const a = 1
a + b
```

This will disable type-checking for the code block and prevent erroring. To show the errors, usually for educational purposes, use the `showErrors` prop:

```tsx allowErrors showErrors
const a = 1
a + b
```

### Breaking Changes

`CodeBlock` now throws an error if the code block is not valid TypeScript. This is to ensure that all code blocks are type-checked and work as expected.
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,14 @@ export function Basic() {
}

export function TypeChecking() {
return <CodeBlock value={`const a = 1; a + b;`} language="ts" />
return (
<CodeBlock
value={`const a = 1; a + b;`}
language="ts"
allowErrors
showErrors
/>
)
}

export function Ordered() {
Expand Down
9 changes: 7 additions & 2 deletions packages/mdxts/src/components/CodeBlock/CodeBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ export type BaseCodeBlockProps = {
/** Whether or not to allow errors. Accepts a boolean or comma-separated list of allowed error codes. */
allowErrors?: boolean | string

/** Whether or not to show error diagnostics. */
showErrors?: boolean

/** Class names to apply to code block elements. Use the `children` prop for full control of styling. */
className?: {
container?: string
Expand Down Expand Up @@ -81,6 +84,7 @@ export async function CodeBlock({
toolbar,
allowCopy,
allowErrors,
showErrors,
...props
}: CodeBlockProps) {
const { sourcePath, sourcePathLine, sourcePathColumn } =
Expand All @@ -105,16 +109,17 @@ export async function CodeBlock({
metadata.value,
metadata.language,
metadata.filename,
allowErrors
allowErrors,
showErrors
)
const contextValue = {
value: metadata.value,
filenameLabel: filename ? metadata.filenameLabel : undefined,
sourcePath: sourcePath
? getSourcePath(sourcePath, sourcePathLine, sourcePathColumn)
: undefined,
tokens,
highlight: lineHighlights,
tokens,
padding,
}

Expand Down
2 changes: 1 addition & 1 deletion packages/mdxts/src/components/CodeBlock/Context.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { CSSProperties } from 'react'
import { createContext } from '../../utils/context'
import { getTokens } from './get-tokens'
import type { getTokens } from './get-tokens'

export const Context = createContext<{
value: string
Expand Down
2 changes: 1 addition & 1 deletion packages/mdxts/src/components/CodeBlock/LineNumbers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React, { Fragment } from 'react'

import { getThemeColors } from '../../index'
import { getContext } from '../../utils/context'
import { getTokens } from './get-tokens'
import type { getTokens } from './get-tokens'
import { Context } from './Context'

/** Renders line numbers for the `CodeBlock` component. */
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { SourceFile } from 'ts-morph'
import { getDiagnosticMessageText } from '@tsxmod/utils'
import { join, sep } from 'node:path'

/** Retrieves diagnostics from a source file and throws an error if errors are found. */
export function getDiagnosticsOrThrow(
sourceFile: SourceFile | undefined,
allowErrors: string | boolean = false,
showErrors: boolean = false
): any[] {
const allowedErrorCodes: number[] =
typeof allowErrors === 'string'
? allowErrors.split(',').map((code) => parseInt(code, 10))
: []

if (!sourceFile) {
return []
}

const diagnostics = sourceFile
.getPreEmitDiagnostics()
.filter((diagnostic) => diagnostic.getSourceFile())

if (showErrors) {
if (allowedErrorCodes.length > 0) {
return diagnostics.filter((diagnostic) => {
return allowedErrorCodes.includes(diagnostic.getCode())
})
}

return diagnostics
}

if (allowErrors === false) {
if (diagnostics.length > 0) {
const workingDirectory = join(process.cwd(), 'mdxts', sep)
const filePath = sourceFile.getFilePath().replace(workingDirectory, '')
const errorDetails = diagnostics
.map((diagnostic) => {
const message = getDiagnosticMessageText(diagnostic.getMessageText())
const line = diagnostic.getLineNumber()
return `line ${line} (${diagnostic.getCode()}): ${message.replaceAll(workingDirectory, '')}`
})
.join('\n\n')

throw new Error(
`[mdxts] CodeBlock type errors found for filename "${filePath}"\n\n${errorDetails}`
)
}

return []
}

if (allowErrors && allowedErrorCodes.length === 0) {
return []
}

return diagnostics.filter((diagnostic) => {
return !allowedErrorCodes.includes(diagnostic.getCode())
})
}
23 changes: 8 additions & 15 deletions packages/mdxts/src/components/CodeBlock/get-tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { getThemeColors } from '../../index'
import { isJsxOnly } from '../../utils/is-jsx-only'
import { getTheme } from '../../utils/get-theme'
import { project } from '../project'
import { getDiagnosticsOrThrow } from './get-diagnostics-or-throw'
import { splitTokenByRanges } from './split-tokens-by-ranges'

export const languageMap = {
Expand Down Expand Up @@ -83,7 +84,8 @@ export async function getTokens(
value: string,
language: Languages = 'plaintext',
filename?: string,
allowErrors?: string | boolean
allowErrors?: string | boolean,
showErrors?: boolean
) {
if (language === 'plaintext') {
return [
Expand All @@ -110,20 +112,11 @@ export async function getTokens(
const isJavaScriptLikeLanguage = ['js', 'jsx', 'ts', 'tsx'].includes(language)
const jsxOnly = isJavaScriptLikeLanguage ? isJsxOnly(value) : false
const sourceFile = filename ? project.getSourceFile(filename) : undefined
const allowedErrorCodes =
typeof allowErrors === 'string'
? allowErrors.split(',').map((code) => parseInt(code))
: []
const sourceFileDiagnostics =
allowedErrorCodes.length === 0 && allowErrors
? []
: sourceFile
? sourceFile
.getPreEmitDiagnostics()
.filter(
(diagnostic) => !allowedErrorCodes.includes(diagnostic.getCode())
)
: []
const sourceFileDiagnostics = getDiagnosticsOrThrow(
sourceFile,
allowErrors,
showErrors
)
const theme = getThemeColors()
const finalLanguage = getLanguage(language)
let { tokens } = highlighter.codeToTokens(
Expand Down
2 changes: 2 additions & 0 deletions packages/mdxts/src/components/MDXComponents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export const MDXComponents = {
const {
allowCopy,
allowErrors,
showErrors,
lineNumbers,
lineHighlights,
toolbar,
Expand All @@ -51,6 +52,7 @@ export const MDXComponents = {
<CodeBlock
allowCopy={allowCopy}
allowErrors={allowErrors}
showErrors={showErrors}
lineNumbers={lineNumbers}
lineHighlights={lineHighlights}
toolbar={toolbar}
Expand Down
8 changes: 5 additions & 3 deletions packages/mdxts/src/next/README.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,12 @@ export function useMDXComponents() {
),
pre: (props) => (
<MDXComponents.pre
className={GeistMono.className}
className={{ container: GeistMono.className }}
style={{
width: 'calc(100% + 2rem)',
margin: '0 -1rem',
container: {
width: 'calc(100% + 2rem)',
margin: '0 -1rem',
},
}}
{...props}
/>
Expand Down
2 changes: 1 addition & 1 deletion site/docs/01.getting-started.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ export default async function Page({ params }: Props) {

All code blocks will automatically be type-checked using the TypeScript compiler to ensure code works as expected. Notice the following will throw a type error because `b` is not defined:

```tsx
```tsx allowErrors showErrors
const a = 1
a + b
```
Expand Down
10 changes: 6 additions & 4 deletions site/docs/04.examples/02.rendering.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,14 @@ For this, you can lean on your specific framework to handle templating and query

```tsx filename="components/[component]/examples/[example]/page.tsx"
import { notFound } from 'next/navigation'
import { allPackages } from 'data'
import { createSource } from 'mdxts'

const allComponents = createSource('components/**/*.tsx')

export const dynamic = 'force-static'

export async function generateStaticParams() {
return (await allPackages.examplePaths()).map((pathname) => ({
return (await allComponents.examplePaths()).map((pathname) => ({
example: pathname,
}))
}
Expand All @@ -60,8 +62,8 @@ export default async function Page({
}: {
params: { example: string[] }
}) {
const singlePackage = await allPackages.get(params.example.slice(0, -1))
const example = await allPackages.getExample(params.example)
const singlePackage = await allComponents.get(params.example.slice(0, -1))
const example = await allComponents.getExample(params.example)

if (singlePackage === undefined || example === undefined) {
return notFound()
Expand Down

0 comments on commit 469b021

Please sign in to comment.