Skip to content

Commit

Permalink
Support strict type checking in MDX
Browse files Browse the repository at this point in the history
The MDX language service now accepts the option `checkMdx`. When this
is enabled, it tells TypeScript to type check the virtual JSX by
inserting a `@ts-check` comment. The language server reads this as the
option `mdx.checkMdx` from `tsconfig.json`.

Closes #352
  • Loading branch information
remcohaszing committed Jan 20, 2024
1 parent 8e26d59 commit 9d51f5e
Show file tree
Hide file tree
Showing 6 changed files with 147 additions and 11 deletions.
7 changes: 7 additions & 0 deletions .changeset/four-cats-scream.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@mdx-js/language-service": patch
"@mdx-js/language-server": patch
"vscode-mdx": patch
---

Support `tsconfig.json` option `mdx.checkMdx`
8 changes: 7 additions & 1 deletion packages/language-server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ connection.onInitialize((parameters) =>

/** @type {PluggableList | undefined} */
let plugins
let checkMdx = false
let jsxImportSource = 'react'

if (configFileName) {
Expand All @@ -87,11 +88,16 @@ connection.onInitialize((parameters) =>
loadPlugin(name, {prefix: 'remark', cwd})
)
)
checkMdx = Boolean(commandLine.raw?.mdx?.checkMdx)
jsxImportSource = commandLine.options.jsxImportSource || jsxImportSource
}

return [
createMdxLanguagePlugin(plugins || defaultPlugins, jsxImportSource)
createMdxLanguagePlugin(
plugins || defaultPlugins,
checkMdx,
jsxImportSource
)
]
}
})
Expand Down
15 changes: 13 additions & 2 deletions packages/language-service/lib/language-plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,18 @@ import {VirtualMdxCode} from './virtual-code.js'
* @param {PluggableList} [plugins]
* A list of remark syntax plugins. Only syntax plugins are supported.
* Transformers are unused.
* @param {boolean} checkMdx
* If true, check MDX files strictly.
* @param {string} jsxImportSource
* The JSX import source to use in the embedded JavaScript file.
* @returns {LanguagePlugin}
* A Volar language plugin to support MDX.
*/
export function createMdxLanguagePlugin(plugins, jsxImportSource = 'react') {
export function createMdxLanguagePlugin(
plugins,
checkMdx = false,
jsxImportSource = 'react'
) {
const processor = unified().use(remarkParse).use(remarkMdx)
if (plugins) {
processor.use(plugins)
Expand All @@ -30,7 +36,12 @@ export function createMdxLanguagePlugin(plugins, jsxImportSource = 'react') {
return {
createVirtualCode(fileId, languageId, snapshot) {
if (languageId === 'mdx') {
return new VirtualMdxCode(snapshot, processor, jsxImportSource)
return new VirtualMdxCode(
snapshot,
processor,
checkMdx,
jsxImportSource
)
}
},

Expand Down
27 changes: 21 additions & 6 deletions packages/language-service/lib/virtual-code.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,15 @@ import {ScriptSnapshot} from './script-snapshot.js'
/**
* Render the content that should be prefixed to the embedded JavaScript file.
*
* @param {boolean} tsCheck
* If true, insert a `@check-js` comment into the virtual JavaScript code.
* @param {string} jsxImportSource
* The string to use for the JSX import source tag.
*/
const jsPrefix = (jsxImportSource) => `/* @jsxRuntime automatic
const jsPrefix = (
tsCheck,
jsxImportSource
) => `${tsCheck ? '// @ts-check\n' : ''}/* @jsxRuntime automatic
@jsxImportSource ${jsxImportSource} */
`

Expand Down Expand Up @@ -76,7 +81,7 @@ export default function MDXContent(props) {
`

const fallback =
jsPrefix('react') + componentStart(false) + '<></>' + componentEnd
jsPrefix(false, 'react') + componentStart(false) + '<></>' + componentEnd

/**
* Visit an mdast tree with and enter and exit callback.
Expand Down Expand Up @@ -275,10 +280,11 @@ function hasAwaitExpression(expression) {
/**
* @param {string} mdx
* @param {Root} ast
* @param {boolean} checkMdx
* @param {string} jsxImportSource
* @returns {VirtualCode[]}
*/
function getEmbeddedCodes(mdx, ast, jsxImportSource) {
function getEmbeddedCodes(mdx, ast, checkMdx, jsxImportSource) {
/** @type {CodeMapping[]} */
const jsMappings = []

Expand Down Expand Up @@ -343,7 +349,7 @@ function getEmbeddedCodes(mdx, ast, jsxImportSource) {
const virtualCodes = []

let hasAwait = false
let esm = jsPrefix(jsxImportSource)
let esm = jsPrefix(checkMdx, jsxImportSource)
let jsx = ''
let markdown = ''
let nextMarkdownSourceStart = 0
Expand Down Expand Up @@ -567,6 +573,7 @@ function getEmbeddedCodes(mdx, ast, jsxImportSource) {
*/
export class VirtualMdxCode {
#processor
#checkMdx
#jsxImportSource

/**
Expand Down Expand Up @@ -616,11 +623,14 @@ export class VirtualMdxCode {
* The original TypeScript snapshot.
* @param {Processor} processor
* The unified processor to use for parsing.
* @param {boolean} checkMdx
* If true, insert a `@check-js` comment into the virtual JavaScript code.
* @param {string} jsxImportSource
* The JSX import source to use in the embedded JavaScript file.
*/
constructor(snapshot, processor, jsxImportSource) {
constructor(snapshot, processor, checkMdx, jsxImportSource) {
this.#processor = processor
this.#checkMdx = checkMdx
this.#jsxImportSource = jsxImportSource
this.snapshot = snapshot
this.update(snapshot)
Expand Down Expand Up @@ -654,7 +664,12 @@ export class VirtualMdxCode {

try {
const ast = this.#processor.parse(mdx)
this.embeddedCodes = getEmbeddedCodes(mdx, ast, this.#jsxImportSource)
this.embeddedCodes = getEmbeddedCodes(
mdx,
ast,
this.#checkMdx,
this.#jsxImportSource
)
this.ast = ast
this.error = undefined
} catch (error) {
Expand Down
90 changes: 89 additions & 1 deletion packages/language-service/test/language-module.js
Original file line number Diff line number Diff line change
Expand Up @@ -2262,8 +2262,96 @@ test('compilation setting overrides', () => {
})
})

test('support checkMdx', () => {
const plugin = createMdxLanguagePlugin(undefined, true)

const snapshot = snapshotFromLines('')

const code = plugin.createVirtualCode('/test.mdx', 'mdx', snapshot)

assert.ok(code instanceof VirtualMdxCode)
assert.equal(code.id, 'mdx')
assert.equal(code.languageId, 'mdx')
assert.ifError(code.error)
assert.equal(code.snapshot, snapshot)
assert.deepEqual(code.mappings, [
{
sourceOffsets: [0],
generatedOffsets: [0],
lengths: [snapshot.getLength()],
data: {
completion: true,
format: true,
navigation: true,
semantic: true,
structure: true,
verification: true
}
}
])
assert.deepEqual(code.embeddedCodes, [
{
embeddedCodes: [],
id: 'jsx',
languageId: 'javascriptreact',
mappings: [],
snapshot: snapshotFromLines(
'// @ts-check',
'/* @jsxRuntime automatic',
'@jsxImportSource react */',
'',
'/**',
' * @deprecated',
' * Do not use.',
' *',
' * @param {{readonly [K in keyof MDXContentProps]: MDXContentProps[K]}} props',
' * The [props](https://mdxjs.com/docs/using-mdx/#props) that have been passed to the MDX component.',
' */',
'function _createMdxContent(props) {',
' return <></>',
'}',
'',
'/**',
' * Render the MDX contents.',
' *',
' * @param {{readonly [K in keyof MDXContentProps]: MDXContentProps[K]}} props',
' * The [props](https://mdxjs.com/docs/using-mdx/#props) that have been passed to the MDX component.',
' */',
'export default function MDXContent(props) {',
' return <_createMdxContent {...props} />',
'}',
'',
'// @ts-ignore',
'/** @typedef {0 extends 1 & Props ? {} : Props} MDXContentProps */',
''
)
},
{
embeddedCodes: [],
id: 'md',
languageId: 'markdown',
mappings: [
{
sourceOffsets: [],
generatedOffsets: [],
lengths: [],
data: {
completion: true,
format: false,
navigation: true,
semantic: true,
structure: true,
verification: true
}
}
],
snapshot: snapshotFromLines('')
}
])
})

test('support custom jsxImportSource', () => {
const plugin = createMdxLanguagePlugin(undefined, 'preact')
const plugin = createMdxLanguagePlugin(undefined, false, 'preact')

const snapshot = snapshotFromLines('')

Expand Down
11 changes: 10 additions & 1 deletion packages/vscode-mdx/tsconfig.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,16 @@
"properties": {
"mdx": {
"title": "MDX language server options",
"$ref": "https://json.schemastore.org/remarkrc.json"
"properties": {
"checkMdx": {
"type": "boolean",
"default": false,
"markdownDescription": "Enable error reporting in type-checked MDX files."
},
"plugins": {
"$ref": "https://json.schemastore.org/remarkrc.json#/properties/plugins"
}
}
}
}
}

0 comments on commit 9d51f5e

Please sign in to comment.