Repository: mdx-js/mdx-analyzer (js)

Issue:
Add option to force enable TypeScript option `checkJs`
### Initial checklist

- [X] I read the [support docs](https://mdxjs.com/community/support/)
- [X] I read the [contributing guide](https://mdxjs.com/community/contribute/)
- [X] I agree to follow the [code of conduct](https://github.com/mdx-js/.github/blob/HEAD/code-of-conduct.md)
- [X] I searched [issues](https://github.com/search?q=user%3Amdx-js&type=Issues) and couldn’t find anything (or linked relevant results below)

### Problem

Under the hood MDX is transformed to JavaScript(X) for type checking. By default TypeScript only checks TypeScript(X). It provides subtle editor features, but no red squiggly lines for type errors. Some users expect the latter.

### Solution

Add a configuration option to force more strict type checking inside MDX files.

### Alternatives

N/A

Pull request description:
Support strict type checking in MDX
### Initial checklist

*   [x] I read the support docs <!-- https://github.com/unifiedjs/.github/blob/main/support.md -->
*   [x] I read the contributing guide <!-- https://github.com/unifiedjs/.github/blob/main/contributing.md -->
*   [x] I agree to follow the code of conduct <!-- https://github.com/unifiedjs/.github/blob/main/code-of-conduct.md -->
*   [x] I searched issues and couldn’t find anything (or linked relevant results below) <!-- https://github.com/search?q=user%3Aunifiedjs&type=Issues -->
*   [x] If applicable, I’ve added docs and tests

### Description of changes

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

<!--do not edit: pr-->


Patch:
diff --git a/.changeset/four-cats-scream.md b/.changeset/four-cats-scream.md
new file mode 100644
index 0000000..3cb7381
--- /dev/null
+++ b/.changeset/four-cats-scream.md
@@ -0,0 +1,7 @@
+---
+"@mdx-js/language-service": patch
+"@mdx-js/language-server": patch
+"vscode-mdx": patch
+---
+
+Support `tsconfig.json` option `mdx.checkMdx`
diff --git a/packages/language-server/index.js b/packages/language-server/index.js
index 5f50985..ec96a16 100755
--- a/packages/language-server/index.js
+++ b/packages/language-server/index.js
@@ -65,6 +65,7 @@ connection.onInitialize((parameters) =>
 
       /** @type {PluggableList | undefined} */
       let plugins
+      let checkMdx = false
       let jsxImportSource = 'react'
 
       if (configFileName) {
@@ -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
+        )
       ]
     }
   })
diff --git a/packages/language-service/lib/language-plugin.js b/packages/language-service/lib/language-plugin.js
index 8371392..1db809a 100644
--- a/packages/language-service/lib/language-plugin.js
+++ b/packages/language-service/lib/language-plugin.js
@@ -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)
@@ -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
+        )
       }
     },
 
diff --git a/packages/language-service/lib/virtual-code.js b/packages/language-service/lib/virtual-code.js
index 17e47aa..29b4e99 100644
--- a/packages/language-service/lib/virtual-code.js
+++ b/packages/language-service/lib/virtual-code.js
@@ -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} */
 `
 
@@ -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.
@@ -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 = []
 
@@ -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
@@ -567,6 +573,7 @@ function getEmbeddedCodes(mdx, ast, jsxImportSource) {
  */
 export class VirtualMdxCode {
   #processor
+  #checkMdx
   #jsxImportSource
 
   /**
@@ -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)
@@ -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) {
diff --git a/packages/vscode-mdx/tsconfig.schema.json b/packages/vscode-mdx/tsconfig.schema.json
index a4f83d2..64cc962 100644
--- a/packages/vscode-mdx/tsconfig.schema.json
+++ b/packages/vscode-mdx/tsconfig.schema.json
@@ -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"
+        }
+      }
     }
   }
 }


Test patch:
diff --git a/packages/language-service/test/language-module.js b/packages/language-service/test/language-module.js
index a66ad7e..7ef3af9 100644
--- a/packages/language-service/test/language-module.js
+++ b/packages/language-service/test/language-module.js
@@ -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('')