Skip to content

Commit

Permalink
add fixImports CodeBlock prop
Browse files Browse the repository at this point in the history
  • Loading branch information
souporserious committed May 1, 2024
1 parent d129076 commit 0b80bf5
Show file tree
Hide file tree
Showing 4 changed files with 92 additions and 44 deletions.
32 changes: 32 additions & 0 deletions .changeset/strange-tigers-dress.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
---
'mdxts': minor
---

Adds a `fixImports` prop to `CodeBlock` to allow fixing imports when the source code references files outside the project and can't resolve correctly:

```tsx
import { CodeBlock } from 'mdxts/components'

const source = `
import { Button } from './Button'
export function BasicUsage() {
return <Button>Click Me</Button>
}
`

export default function Page() {
return <CodeBlock fixImports value={source} />
}
```

An example of this is when rendering a source file that imports a module from a package that is not in the immediate project. The `fixImports` prop will attempt to fix these broken imports using installed packages if a match is found:

```diff
--import { Button } from './Button'
++import { Button } from 'design-system'

export function BasicUsage() {
return <Button>Click Me</Button>
}
```
5 changes: 5 additions & 0 deletions packages/mdxts/src/components/CodeBlock/CodeBlock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ export type BaseCodeBlockProps = {
/** Whether or not to show error diagnostics. */
showErrors?: boolean

/** Whether or not to attempt to fix broken imports. Useful for code using imports outside of the project. */
fixImports?: 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 @@ -85,6 +88,7 @@ export async function CodeBlock({
allowCopy,
allowErrors,
showErrors,
fixImports,
...props
}: CodeBlockProps) {
const { sourcePath, sourcePathLine, sourcePathColumn } =
Expand All @@ -103,6 +107,7 @@ export async function CodeBlock({
filename,
language,
allowErrors,
fixImports,
...options,
})
const tokens = await getTokens(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ type BaseParseMetadataOptions = {
filename?: string
language?: Languages
allowErrors?: boolean | string
fixImports?: boolean
}

export type ParseMetadataOptions = BaseParseMetadataOptions &
Expand All @@ -31,6 +32,7 @@ export async function parseSourceTextMetadata({
filename: filenameProp,
language,
allowErrors = false,
fixImports,
...props
}: ParseMetadataOptions) {
let finalValue: string = ''
Expand Down Expand Up @@ -130,55 +132,63 @@ export async function parseSourceTextMetadata({
overwrite: true,
})

const importErrorCode = 2307
const shouldEmitDiagnostics =
typeof allowErrors === 'string'
? !allowErrors.includes(importErrorCode.toString())
: allowErrors === false

if (shouldEmitDiagnostics) {
// Identify and collect missing imports/types to try and resolve them.
// This is specifically the case for examples since they import files relative to the package.
const diagnostics = sourceFile.getPreEmitDiagnostics()

sourceFile
.getImportDeclarations()
.filter((importDeclaration) => {
return diagnostics.some((diagnostic) => {
const diagnosticStart = diagnostic.getStart()
if (diagnosticStart === undefined) {
return false
}
return (
diagnostic.getCode() === importErrorCode &&
diagnosticStart >= importDeclaration.getStart() &&
diagnosticStart <= importDeclaration.getEnd()
)
// Fixes incorrect imports by removing them and trying to resolve them.
// This is the case with examples loaded from outside the main project directory.
if (fixImports) {
const importErrorCode = 2307
const shouldEmitDiagnostics =
typeof allowErrors === 'string'
? !allowErrors.includes(importErrorCode.toString())
: allowErrors === false

if (shouldEmitDiagnostics) {
// Identify and collect missing imports/types to try and resolve them.
// This is specifically the case for examples since they import files relative to the package.
const diagnostics = sourceFile.getPreEmitDiagnostics()

sourceFile
.getImportDeclarations()
.filter((importDeclaration) => {
return diagnostics.some((diagnostic) => {
const diagnosticStart = diagnostic.getStart()
if (diagnosticStart === undefined) {
return false
}
return (
diagnostic.getCode() === importErrorCode &&
diagnosticStart >= importDeclaration.getStart() &&
diagnosticStart <= importDeclaration.getEnd()
)
})
})
.forEach((importDeclaration) => {
importDeclaration.remove()
})
}

// attempt to fix the removed imports and any other missing imports
sourceFile.fixMissingImports()

const importDeclarations = sourceFile.getImportDeclarations()

if (shouldEmitDiagnostics) {
// remap relative module specifiers to package imports if possible
// e.g. `import { getTheme } from '../../mdxts/src/components'` -> `import { getTheme } from 'mdxts/components'`
importDeclarations.forEach((importDeclaration) => {
if (importDeclaration.isModuleSpecifierRelative()) {
const importSpecifier =
getPathRelativeToPackage(importDeclaration)
importDeclaration.setModuleSpecifier(importSpecifier)
}
})
.forEach((importDeclaration) => {
importDeclaration.remove()
})
}

// attempt to fix the removed imports and any other missing imports
sourceFile.fixMissingImports()

const importDeclarations = sourceFile.getImportDeclarations()

if (shouldEmitDiagnostics) {
// remap relative module specifiers to package imports if possible
// e.g. `import { getTheme } from '../../mdxts/src/components'` -> `import { getTheme } from 'mdxts/components'`
importDeclarations.forEach((importDeclaration) => {
if (importDeclaration.isModuleSpecifierRelative()) {
const importSpecifier = getPathRelativeToPackage(importDeclaration)
importDeclaration.setModuleSpecifier(importSpecifier)
}
})
}
} else if (jsxOnly) {
// Since JSX only code blocks don't have imports, attempt to fix them.
sourceFile.fixMissingImports()
}

// If no imports or exports add an empty export declaration to coerce TypeScript to treat the file as a module
const hasImports = importDeclarations.length > 0
const hasImports = sourceFile.getImportDeclarations().length > 0
const hasExports = sourceFile.getExportDeclarations().length > 0

if (!hasImports && !hasExports) {
Expand Down
1 change: 1 addition & 0 deletions site/app/examples/[...example]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ export default async function Page({
/>
<div className={styles.container}>
<CodeBlock
fixImports
lineNumbers
value={example.sourceText}
language="tsx"
Expand Down

0 comments on commit 0b80bf5

Please sign in to comment.