Repository: mdx-js/mdx-analyzer
Pull request #382 (2023-12-22 19:53:01)

Inject JSX comments into 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

This way we don’t have to depend on patching TypeScript compiler options.

<!--do not edit: pr-->


changeset-bot[bot]: ###  ⚠️  No Changeset found

Latest commit: 53376b0e88726ecba98fde87b4cbecbafcaf1508

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. **If these changes should result in a version bump, you need to add a changeset.**

<details><summary>This PR includes no changesets</summary>

  When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

</details>

[Click here to learn what changesets are, and how to add one](https://github.com/changesets/changesets/blob/main/docs/adding-a-changeset.md).

[Click here if you're a maintainer who wants to add a changeset to this PR](https://github.com/mdx-js/mdx-analyzer/new/jsx-import-source?filename=.changeset/itchy-tigers-dream.md&value=---%0A%22%40mdx-js%2Flanguage-service%22%3A%20patch%0A---%0A%0AInject%20JSX%20comments%20into%20MDX%0A)



Patch:
diff --git a/packages/language-server/index.js b/packages/language-server/index.js
index 5984492..8a67cc5 100755
--- a/packages/language-server/index.js
+++ b/packages/language-server/index.js
@@ -64,6 +64,7 @@ connection.onInitialize((parameters) =>
 
       /** @type {PluggableList | undefined} */
       let plugins
+      let jsxImportSource = 'react'
 
       if (configFileName) {
         const cwd = path.dirname(configFileName)
@@ -85,9 +86,12 @@ connection.onInitialize((parameters) =>
               loadPlugin(name, {prefix: 'remark', cwd})
             )
         )
+        jsxImportSource = commandLine.options.jsxImportSource || jsxImportSource
       }
 
-      return [createMdxLanguagePlugin(plugins || defaultPlugins)]
+      return [
+        createMdxLanguagePlugin(plugins || defaultPlugins, jsxImportSource)
+      ]
     }
   })
 )
diff --git a/packages/language-service/lib/language-plugin.js b/packages/language-service/lib/language-plugin.js
index 1b67132..c0f3171 100644
--- a/packages/language-service/lib/language-plugin.js
+++ b/packages/language-service/lib/language-plugin.js
@@ -14,10 +14,12 @@ import {VirtualMdxFile} from './virtual-file.js'
  * @param {PluggableList} [plugins]
  *   A list of remark syntax plugins. Only syntax plugins are supported.
  *   Transformers are unused.
+ * @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) {
+export function createMdxLanguagePlugin(plugins, jsxImportSource = 'react') {
   const processor = unified().use(remarkParse).use(remarkMdx)
   if (plugins) {
     processor.use(plugins)
@@ -28,7 +30,12 @@ export function createMdxLanguagePlugin(plugins) {
   return {
     createVirtualFile(fileName, languageId, snapshot) {
       if (languageId === 'mdx') {
-        return new VirtualMdxFile(fileName, snapshot, processor)
+        return new VirtualMdxFile(
+          fileName,
+          snapshot,
+          processor,
+          jsxImportSource
+        )
       }
     },
 
@@ -51,13 +58,6 @@ export function createMdxLanguagePlugin(plugins) {
         return {
           ...host,
           getCompilationSettings: () => ({
-            // Default to the JSX automatic runtime, because that’s what MDX does.
-            jsx: 4,
-            // Set these defaults to match MDX if the user explicitly sets the classic runtime.
-            jsxFactory: 'React.createElement',
-            jsxFragmentFactory: 'React.Fragment',
-            // Set this default to match MDX if the user overrides the import source.
-            jsxImportSource: 'react',
             ...host.getCompilationSettings(),
             // Always allow JS for type checking.
             allowJs: true
diff --git a/packages/language-service/lib/virtual-file.js b/packages/language-service/lib/virtual-file.js
index f759ed0..280c7f3 100644
--- a/packages/language-service/lib/virtual-file.js
+++ b/packages/language-service/lib/virtual-file.js
@@ -15,6 +15,16 @@
 import {walk} from 'estree-walker'
 import {ScriptSnapshot} from './script-snapshot.js'
 
+/**
+ * Render the content that should be prefixed to the embedded JavaScript file.
+ *
+ * @param {string} jsxImportSource
+ *   The string to use for the JSX import source tag.
+ */
+const jsPrefix = (jsxImportSource) => `/* @jsxRuntime automatic
+@jsxImportSource ${jsxImportSource} */
+`
+
 /**
  * @param {string} propsName
  */
@@ -65,7 +75,8 @@ export default function MDXContent(props) {
 /** @typedef {0 extends 1 & Props ? {} : Props} MDXContentProps */
 `
 
-const fallback = componentStart(false) + '<></>' + componentEnd
+const fallback =
+  jsPrefix('react') + componentStart(false) + '<></>' + componentEnd
 
 /**
  * Visit an mdast tree with and enter and exit callback.
@@ -265,9 +276,10 @@ function hasAwaitExpression(expression) {
  * @param {string} fileName
  * @param {string} mdx
  * @param {Root} ast
+ * @param {string} jsxImportSource
  * @returns {VirtualFile[]}
  */
-function getEmbeddedFiles(fileName, mdx, ast) {
+function getEmbeddedFiles(fileName, mdx, ast, jsxImportSource) {
   /** @type {Mapping[]} */
   const jsMappings = []
 
@@ -332,7 +344,7 @@ function getEmbeddedFiles(fileName, mdx, ast) {
   const virtualFiles = []
 
   let hasAwait = false
-  let esm = ''
+  let esm = jsPrefix(jsxImportSource)
   let jsx = ''
   let markdown = ''
   let nextMarkdownSourceStart = 0
@@ -561,6 +573,7 @@ function getEmbeddedFiles(fileName, mdx, ast) {
  */
 export class VirtualMdxFile {
   #processor
+  #jsxImportSource
 
   /**
    * The virtual files embedded in the MDX file.
@@ -597,9 +610,12 @@ export class VirtualMdxFile {
    *   The original TypeScript snapshot.
    * @param {Processor} processor
    *   The unified processor to use for parsing.
+   * @param {string} jsxImportSource
+   *   The JSX import source to use in the embedded JavaScript file.
    */
-  constructor(fileName, snapshot, processor) {
+  constructor(fileName, snapshot, processor, jsxImportSource) {
     this.#processor = processor
+    this.#jsxImportSource = jsxImportSource
     this.fileName = fileName
     this.snapshot = snapshot
     this.update(snapshot)
@@ -633,7 +649,12 @@ export class VirtualMdxFile {
 
     try {
       const ast = this.#processor.parse(mdx)
-      this.embeddedFiles = getEmbeddedFiles(this.fileName, mdx, ast)
+      this.embeddedFiles = getEmbeddedFiles(
+        this.fileName,
+        mdx,
+        ast,
+        this.#jsxImportSource
+      )
       this.error = undefined
     } catch (error) {
       this.error = /** @type {VFileMessage} */ (error)


Test patch:
diff --git a/packages/language-server/test/completion.test.js b/packages/language-server/test/completion.test.js
index ab1a339..7396d31 100644
--- a/packages/language-server/test/completion.test.js
+++ b/packages/language-server/test/completion.test.js
@@ -37,7 +37,7 @@ test('support completion in ESM', async () => {
       original: {
         data: {
           fileName: fixturePath('node16/completion.mdx.jsx'),
-          offset: 30,
+          offset: 81,
           originalItem: {name: 'Boolean'},
           uri: fixtureUri('node16/completion.mdx.jsx')
         }
@@ -57,7 +57,7 @@ test('support completion in ESM', async () => {
     commitCharacters: ['.', ',', ';', '('],
     data: {
       fileName: fixturePath('node16/completion.mdx.jsx'),
-      offset: 30,
+      offset: 81,
       originalItem: {name: 'Boolean'},
       uri: fixtureUri('node16/completion.mdx.jsx')
     },
@@ -93,7 +93,7 @@ test('support completion in JSX', async () => {
       original: {
         data: {
           fileName: fixturePath('node16/completion.mdx.jsx'),
-          offset: 67,
+          offset: 118,
           originalItem: {name: 'Boolean'},
           uri: fixtureUri('node16/completion.mdx.jsx')
         }
@@ -113,7 +113,7 @@ test('support completion in JSX', async () => {
     commitCharacters: ['.', ',', ';', '('],
     data: {
       fileName: fixturePath('node16/completion.mdx.jsx'),
-      offset: 67,
+      offset: 118,
       originalItem: {name: 'Boolean'},
       uri: fixtureUri('node16/completion.mdx.jsx')
     },
diff --git a/packages/language-service/test/language-module.js b/packages/language-service/test/language-module.js
index e4a3c6d..00d3b8d 100644
--- a/packages/language-service/test/language-module.js
+++ b/packages/language-service/test/language-module.js
@@ -46,7 +46,7 @@ test('create virtual file w/ mdxjsEsm', () => {
       mappings: [
         {
           sourceOffsets: [0],
-          generatedOffsets: [0],
+          generatedOffsets: [51],
           lengths: [34],
           data: {
             completion: true,
@@ -59,6 +59,8 @@ test('create virtual file w/ mdxjsEsm', () => {
         }
       ],
       snapshot: snapshotFromLines(
+        '/* @jsxRuntime automatic',
+        '@jsxImportSource react */',
         'import {Planet} from "./Planet.js"',
         '',
         '/**',
@@ -149,7 +151,7 @@ test('create virtual file w/o MDX layout in case of named re-export', () => {
       mappings: [
         {
           sourceOffsets: [0],
-          generatedOffsets: [0],
+          generatedOffsets: [51],
           lengths: [33],
           data: {
             completion: true,
@@ -162,6 +164,8 @@ test('create virtual file w/o MDX layout in case of named re-export', () => {
         }
       ],
       snapshot: snapshotFromLines(
+        '/* @jsxRuntime automatic',
+        '@jsxImportSource react */',
         'export {named} from "./Layout.js"',
         '',
         '/**',
@@ -255,7 +259,7 @@ test('create virtual file w/ MDX layout in case of default re-export', () => {
       mappings: [
         {
           sourceOffsets: [0, 15],
-          generatedOffsets: [0, 8],
+          generatedOffsets: [51, 59],
           lengths: [8, 20],
           data: {
             completion: true,
@@ -268,6 +272,8 @@ test('create virtual file w/ MDX layout in case of default re-export', () => {
         }
       ],
       snapshot: snapshotFromLines(
+        '/* @jsxRuntime automatic',
+        '@jsxImportSource react */',
         'export {} from "./Layout.js"',
         'import {default as MDXLayout} from "./Layout.js"',
         '/**',
@@ -361,7 +367,7 @@ test('create virtual file w/ MDX layout in case of named and default re-export',
       mappings: [
         {
           sourceOffsets: [0, 22],
-          generatedOffsets: [0, 15],
+          generatedOffsets: [51, 66],
           lengths: [15, 20],
           data: {
             completion: true,
@@ -374,6 +380,8 @@ test('create virtual file w/ MDX layout in case of named and default re-export',
         }
       ],
       snapshot: snapshotFromLines(
+        '/* @jsxRuntime automatic',
+        '@jsxImportSource react */',
         'export {named, } from "./Layout.js"',
         'import {default as MDXLayout} from "./Layout.js"',
         '/**',
@@ -467,7 +475,7 @@ test('create virtual file w/ MDX layout in case of default and named re-export',
       mappings: [
         {
           sourceOffsets: [0, 16],
-          generatedOffsets: [0, 8],
+          generatedOffsets: [51, 59],
           lengths: [8, 26],
           data: {
             completion: true,
@@ -480,6 +488,8 @@ test('create virtual file w/ MDX layout in case of default and named re-export',
         }
       ],
       snapshot: snapshotFromLines(
+        '/* @jsxRuntime automatic',
+        '@jsxImportSource react */',
         'export { named} from "./Layout.js"',
         'import {default as MDXLayout} from "./Layout.js"',
         '/**',
@@ -570,7 +580,7 @@ test('create virtual file w/ MDX layout in case of a default exported arrow func
       mappings: [
         {
           sourceOffsets: [15],
-          generatedOffsets: [670],
+          generatedOffsets: [721],
           lengths: [8],
           data: {
             completion: true,
@@ -583,6 +593,8 @@ test('create virtual file w/ MDX layout in case of a default exported arrow func
         }
       ],
       snapshot: snapshotFromLines(
+        '/* @jsxRuntime automatic',
+        '@jsxImportSource react */',
         '',
         '/** @typedef {MDXContentProps & { children: JSX.Element }} MDXLayoutProps */',
         '',
@@ -690,7 +702,7 @@ test('create virtual file w/ MDX layout in case of a default exported function d
       mappings: [
         {
           sourceOffsets: [15],
-          generatedOffsets: [670],
+          generatedOffsets: [721],
           lengths: [23],
           data: {
             completion: true,
@@ -703,6 +715,8 @@ test('create virtual file w/ MDX layout in case of a default exported function d
         }
       ],
       snapshot: snapshotFromLines(
+        '/* @jsxRuntime automatic',
+        '@jsxImportSource react */',
         '',
         '/** @typedef {MDXContentProps & { children: JSX.Element }} MDXLayoutProps */',
         '',
@@ -807,7 +821,7 @@ test('create virtual file w/ MDX layout in case of a default exported constant',
       mappings: [
         {
           sourceOffsets: [15],
-          generatedOffsets: [19],
+          generatedOffsets: [70],
           lengths: [6],
           data: {
             completion: true,
@@ -820,6 +834,8 @@ test('create virtual file w/ MDX layout in case of a default exported constant',
         }
       ],
       snapshot: snapshotFromLines(
+        '/* @jsxRuntime automatic',
+        '@jsxImportSource react */',
         '',
         'const MDXLayout = "main"',
         '',
@@ -914,7 +930,7 @@ test('create virtual file w/ MDX layout and matching argument name', () => {
       mappings: [
         {
           sourceOffsets: [15],
-          generatedOffsets: [675],
+          generatedOffsets: [726],
           lengths: [33],
           data: {
             completion: true,
@@ -927,6 +943,8 @@ test('create virtual file w/ MDX layout and matching argument name', () => {
         }
       ],
       snapshot: snapshotFromLines(
+        '/* @jsxRuntime automatic',
+        '@jsxImportSource react */',
         '',
         '/** @typedef {MDXContentProps & { children: JSX.Element }} MDXLayoutProps */',
         '',
@@ -1035,7 +1053,7 @@ test('create virtual file w/ MDX layout in case of a default export followed by
       mappings: [
         {
           sourceOffsets: [15, 39],
-          generatedOffsets: [670, 694],
+          generatedOffsets: [721, 745],
           lengths: [23, 26],
           data: {
             completion: true,
@@ -1048,6 +1066,8 @@ test('create virtual file w/ MDX layout in case of a default export followed by
         }
       ],
       snapshot: snapshotFromLines(
+        '/* @jsxRuntime automatic',
+        '@jsxImportSource react */',
         '',
         '/** @typedef {MDXContentProps & { children: JSX.Element }} MDXLayoutProps */',
         '',
@@ -1157,7 +1177,7 @@ test('create virtual file w/ MDX layout in case of a default export preceded by
       mappings: [
         {
           sourceOffsets: [0, 42],
-          generatedOffsets: [0, 697],
+          generatedOffsets: [51, 748],
           lengths: [26, 23],
           data: {
             completion: true,
@@ -1170,6 +1190,8 @@ test('create virtual file w/ MDX layout in case of a default export preceded by
         }
       ],
       snapshot: snapshotFromLines(
+        '/* @jsxRuntime automatic',
+        '@jsxImportSource react */',
         'export function named() {}',
         '',
         '/** @typedef {MDXContentProps & { children: JSX.Element }} MDXLayoutProps */',
@@ -1275,7 +1297,7 @@ test('create virtual file w/ mdxFlowExpression', () => {
       mappings: [
         {
           sourceOffsets: [0],
-          generatedOffsets: [271],
+          generatedOffsets: [322],
           lengths: [9],
           data: {
             completion: true,
@@ -1288,6 +1310,8 @@ test('create virtual file w/ mdxFlowExpression', () => {
         }
       ],
       snapshot: snapshotFromLines(
+        '/* @jsxRuntime automatic',
+        '@jsxImportSource react */',
         '',
         '/**',
         ' * @deprecated',
@@ -1384,7 +1408,7 @@ test('create virtual file w/ mdxJsxFlowElement w/ children', () => {
       mappings: [
         {
           sourceOffsets: [0, 57],
-          generatedOffsets: [271, 289],
+          generatedOffsets: [322, 340],
           lengths: [9, 8],
           data: {
             completion: true,
@@ -1397,6 +1421,8 @@ test('create virtual file w/ mdxJsxFlowElement w/ children', () => {
         }
       ],
       snapshot: snapshotFromLines(
+        '/* @jsxRuntime automatic',
+        '@jsxImportSource react */',
         '',
         '/**',
         ' * @deprecated',
@@ -1490,7 +1516,7 @@ test('create virtual file w/ mdxJsxFlowElement w/o children', () => {
       mappings: [
         {
           sourceOffsets: [0],
-          generatedOffsets: [271],
+          generatedOffsets: [322],
           lengths: [7],
           data: {
             completion: true,
@@ -1503,6 +1529,8 @@ test('create virtual file w/ mdxJsxFlowElement w/o children', () => {
         }
       ],
       snapshot: snapshotFromLines(
+        '/* @jsxRuntime automatic',
+        '@jsxImportSource react */',
         '',
         '/**',
         ' * @deprecated',
@@ -1595,7 +1623,7 @@ test('create virtual file w/ mdxJsxTextElement', () => {
       mappings: [
         {
           sourceOffsets: [2],
-          generatedOffsets: [277],
+          generatedOffsets: [328],
           lengths: [7],
           data: {
             completion: true,
@@ -1608,6 +1636,8 @@ test('create virtual file w/ mdxJsxTextElement', () => {
         }
       ],
       snapshot: snapshotFromLines(
+        '/* @jsxRuntime automatic',
+        '@jsxImportSource react */',
         '',
         '/**',
         ' * @deprecated',
@@ -1696,7 +1726,7 @@ test('create virtual file w/ mdxTextExpression', () => {
       },
       mappings: [
         {
-          generatedOffsets: [277],
+          generatedOffsets: [328],
           sourceOffsets: [4],
           lengths: [9],
           data: {
@@ -1710,6 +1740,8 @@ test('create virtual file w/ mdxTextExpression', () => {
         }
       ],
       snapshot: snapshotFromLines(
+        '/* @jsxRuntime automatic',
+        '@jsxImportSource react */',
         '',
         '/**',
         ' * @deprecated',
@@ -1801,7 +1833,7 @@ test('create virtual file w/ async mdxTextExpression', () => {
       },
       mappings: [
         {
-          generatedOffsets: [283],
+          generatedOffsets: [334],
           sourceOffsets: [4],
           lengths: [32],
           data: {
@@ -1815,6 +1847,8 @@ test('create virtual file w/ async mdxTextExpression', () => {
         }
       ],
       snapshot: snapshotFromLines(
+        '/* @jsxRuntime automatic',
+        '@jsxImportSource react */',
         '',
         '/**',
         ' * @deprecated',
@@ -1909,6 +1943,8 @@ test('create virtual file w/ dedented markdown content', () => {
       },
       mappings: [],
       snapshot: snapshotFromLines(
+        '/* @jsxRuntime automatic',
+        '@jsxImportSource react */',
         '',
         '/**',
         ' * @deprecated',
@@ -2000,6 +2036,8 @@ test('create virtual file w/ syntax error', () => {
       languageId: 'javascriptreact',
       mappings: [],
       snapshot: snapshotFromLines(
+        '/* @jsxRuntime automatic',
+        '@jsxImportSource react */',
         '',
         '/**',
         ' * @deprecated',
@@ -2074,6 +2112,8 @@ test('create virtual file w/ yaml frontmatter', () => {
       languageId: 'javascriptreact',
       mappings: [],
       snapshot: snapshotFromLines(
+        '/* @jsxRuntime automatic',
+        '@jsxImportSource react */',
         '',
         '/**',
         ' * @deprecated',
@@ -2189,6 +2229,8 @@ test('update virtual file', () => {
       languageId: 'javascriptreact',
       mappings: [],
       snapshot: snapshotFromLines(
+        '/* @jsxRuntime automatic',
+        '@jsxImportSource react */',
         '',
         '/**',
         ' * @deprecated',
@@ -2254,11 +2296,7 @@ test('compilation setting defaults', () => {
   const compilerOptions = host?.getCompilationSettings()
 
   assert.deepEqual(compilerOptions, {
-    allowJs: true,
-    jsx: typescript.JsxEmit.ReactJSX,
-    jsxFactory: 'React.createElement',
-    jsxFragmentFactory: 'React.Fragment',
-    jsxImportSource: 'react'
+    allowJs: true
   })
 })
 
@@ -2287,6 +2325,96 @@ test('compilation setting overrides', () => {
   })
 })
 
+test('support custom jsxImportSource', () => {
+  const plugin = createMdxLanguagePlugin(undefined, 'preact')
+
+  const snapshot = snapshotFromLines('')
+
+  const file = plugin.createVirtualFile('/test.mdx', 'mdx', snapshot)
+
+  assert.ok(file instanceof VirtualMdxFile)
+  assert.equal(file.fileName, '/test.mdx')
+  assert.equal(file.languageId, 'mdx')
+  assert.ifError(file.error)
+  assert.equal(file.snapshot, snapshot)
+  assert.deepEqual(file.mappings, [
+    {
+      sourceOffsets: [0],
+      generatedOffsets: [0],
+      lengths: [snapshot.getLength()],
+      data: {
+        completion: true,
+        format: true,
+        navigation: true,
+        semantic: true,
+        structure: true,
+        verification: true
+      }
+    }
+  ])
+  assert.deepEqual(file.embeddedFiles, [
+    {
+      embeddedFiles: [],
+      fileName: '/test.mdx.jsx',
+      languageId: 'javascriptreact',
+      mappings: [],
+      snapshot: snapshotFromLines(
+        '/* @jsxRuntime automatic',
+        '@jsxImportSource preact */',
+        '',
+        '/**',
+        ' * @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 */',
+        ''
+      ),
+      typescript: {
+        scriptKind: typescript.ScriptKind.JSX
+      }
+    },
+    {
+      embeddedFiles: [],
+      fileName: '/test.mdx.md',
+      languageId: 'markdown',
+      mappings: [
+        {
+          sourceOffsets: [],
+          generatedOffsets: [],
+          lengths: [],
+          data: {
+            completion: true,
+            format: false,
+            navigation: true,
+            semantic: true,
+            structure: true,
+            verification: true
+          }
+        }
+      ],
+      snapshot: snapshotFromLines('')
+    }
+  ])
+})
+
 /**
  * @param {string[]} lines
  * @returns {typescript.IScriptSnapshot}