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

Issue:
Support the `components` prop
### 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

MDX allows to use the `components` prop to pass custom components.

```mdx
{/* Greeting.mdx */}

Hello <Person name={name} />
```

```jsx
import Greeting from './Greeting.mdx'

<Greeting components={{ Person: props => props.name }} name="Gary" />
```

The MDX code will be transformed to the following JSX before sending to TypeScript:

```jsx
{/* Greeting.mdx */}

/**
 * Render the MDX contents.
 *
 * @param {MDXContentProps} props
 *   The props that have been passed to the MDX component.
 */
export default function MDXContent() {
  return <>
{/* Greeting.mdx */}
      <Person name={name} />
<>
}

// @ts-ignore
/** @typedef {Props} MDXContentProps */
```

### Solution

We can extract all MDX elements and generate types for them to some extent. Components may be injected by a provider hook, so they should always be optional.

```js
/**
 * @typedef {Props & { readonly components?: {
 *   readonly Person?: ((props: { name?: any }) => JSX.Element | null) | JSX.ClassComponent<{ name?: any }> | // XXX
 * } }} MDXContentProps
 */
```

- Lower case components and `JSX.IntrinsicElements`
- Function components
- Class components
- Components in nested objects

### Alternatives

:shrug: 

Pull request description:
Support injected components inside MDX expressions
### 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

Closes #260

<!--do not edit: pr-->


Patch:
diff --git a/.changeset/large-dolls-obey.md b/.changeset/large-dolls-obey.md
index b317e55..1f0e92f 100644
--- a/.changeset/large-dolls-obey.md
+++ b/.changeset/large-dolls-obey.md
@@ -4,4 +4,4 @@
 "vscode-mdx": patch
 ---
 
-Support the `components` prop for MDX JSX tags.
+Support the `components` prop for MDX JSX tags and JSX expressions.
diff --git a/package.json b/package.json
index 56a2fad..219909f 100644
--- a/package.json
+++ b/package.json
@@ -44,6 +44,7 @@
   "xo": {
     "prettier": true,
     "rules": {
+      "complexity": "off",
       "max-params": "off",
       "unicorn/prevent-abbreviations": "off"
     }
diff --git a/packages/language-service/lib/virtual-code.js b/packages/language-service/lib/virtual-code.js
index eaedb20..ea7063f 100644
--- a/packages/language-service/lib/virtual-code.js
+++ b/packages/language-service/lib/virtual-code.js
@@ -284,39 +284,6 @@ function processExports(mdx, node, mapping, esm) {
   return esm + '\n'
 }
 
-/**
- * @param {Program | undefined} expression
- * @returns {boolean}
- */
-function hasAwaitExpression(expression) {
-  let awaitExpression = false
-  if (expression) {
-    walk(expression, {
-      enter(node) {
-        if (
-          awaitExpression ||
-          node.type === 'ArrowFunctionExpression' ||
-          node.type === 'FunctionDeclaration' ||
-          node.type === 'FunctionExpression'
-        ) {
-          this.skip()
-          return
-        }
-
-        if (
-          node.type === 'AwaitExpression' ||
-          (node.type === 'ForOfStatement' && node.await)
-        ) {
-          awaitExpression = true
-          this.skip()
-        }
-      }
-    })
-  }
-
-  return awaitExpression
-}
-
 /**
  * @param {string} mdx
  * @param {Root} ast
@@ -472,6 +439,73 @@ function getEmbeddedCodes(mdx, ast, checkMdx, jsxImportSource) {
     updateMarkdownFromOffsets(startOffset, endOffset)
   }
 
+  /**
+   * @param {Program} program
+   * @param {number} lastIndex
+   * @returns {number}
+   */
+  function processJsxExpression(program, lastIndex) {
+    let newIndex = lastIndex
+    let functionNesting = 0
+    walk(program, {
+      enter(node) {
+        switch (node.type) {
+          case 'JSXIdentifier': {
+            if (!isInjectableComponent(node.name, variables)) {
+              return
+            }
+
+            jsx =
+              addOffset(jsxMapping, mdx, jsx, newIndex, node.start) +
+              '_components.'
+            newIndex = node.start
+            break
+          }
+
+          case 'ArrowFunctionExpression':
+          case 'FunctionDeclaration':
+          case 'FunctionExpression': {
+            functionNesting++
+            break
+          }
+
+          case 'AwaitExpression': {
+            if (!functionNesting) {
+              hasAwait = true
+            }
+
+            break
+          }
+
+          case 'ForOfStatement': {
+            if (!functionNesting) {
+              hasAwait ||= node.await
+            }
+
+            break
+          }
+
+          default:
+        }
+      },
+
+      leave(node) {
+        switch (node.type) {
+          case 'ArrowFunctionExpression':
+          case 'FunctionDeclaration':
+          case 'FunctionExpression': {
+            functionNesting--
+            break
+          }
+
+          default:
+        }
+      }
+    })
+
+    return newIndex
+  }
+
   visit(
     ast,
     (node) => {
@@ -525,20 +559,37 @@ function getEmbeddedCodes(mdx, ast, checkMdx, jsxImportSource) {
           }
 
           updateMarkdownFromOffsets(start, end)
+
+          let lastIndex = start + 1
+          jsx = addOffset(jsxMapping, mdx, jsx + jsxIndent, start, lastIndex)
           if (isInjectableComponent(node.name, variables)) {
-            const openingStart = start + 1
+            jsx += '_components.'
+          }
+
+          if (node.name) {
             jsx = addOffset(
               jsxMapping,
               mdx,
-              addOffset(jsxMapping, mdx, jsx + jsxIndent, start, openingStart) +
-                '_components.',
-              openingStart,
-              end
+              jsx,
+              lastIndex,
+              lastIndex + node.name.length
             )
-          } else {
-            jsx = addOffset(jsxMapping, mdx, jsx + jsxIndent, start, end)
+            lastIndex += node.name.length
+          }
+
+          for (const attribute of node.attributes) {
+            if (typeof attribute.value !== 'object') {
+              continue
+            }
+
+            const program = attribute.value?.data?.estree
+
+            if (program) {
+              lastIndex = processJsxExpression(program, lastIndex)
+            }
           }
 
+          jsx = addOffset(jsxMapping, mdx, jsx, lastIndex, end)
           break
         }
 
@@ -546,17 +597,15 @@ function getEmbeddedCodes(mdx, ast, checkMdx, jsxImportSource) {
         case 'mdxTextExpression': {
           updateMarkdownFromNode(node)
           const program = node.data?.estree
+          jsx += jsxIndent
 
-          if (program?.body.length === 0) {
-            jsx = addOffset(jsxMapping, mdx, jsx + jsxIndent, start, start + 1)
+          if (program?.body.length) {
+            const newIndex = processJsxExpression(program, start)
+            jsx = addOffset(jsxMapping, mdx, jsx, newIndex, end)
+          } else {
+            jsx = addOffset(jsxMapping, mdx, jsx, start, start + 1)
             jsx = addOffset(jsxMapping, mdx, jsx, end - 1, end)
             esm = addOffset(esmMapping, mdx, esm, start + 1, end - 1) + '\n'
-          } else {
-            if (program) {
-              hasAwait ||= hasAwaitExpression(program)
-            }
-
-            jsx = addOffset(jsxMapping, mdx, jsx + jsxIndent, start, end)
           }
 
           break


Test patch:
diff --git a/packages/language-service/test/language-module.js b/packages/language-service/test/language-module.js
index 2ec4d9d..026c2b8 100644
--- a/packages/language-service/test/language-module.js
+++ b/packages/language-service/test/language-module.js
@@ -1476,6 +1476,308 @@ test('create virtual code w/ mdxFlowExpression', () => {
   ])
 })
 
+test('create virtual code w/ prefixed JSX expressions for mdxFlowExpression', () => {
+  const plugin = createMdxLanguagePlugin()
+
+  const snapshot = snapshotFromLines(
+    'export function Local() {}',
+    '',
+    '{<div />}',
+    '{<div>{""}</div>}',
+    '{<Injected />}',
+    '{<Injected>{""}</Injected>}',
+    '{<Local />}',
+    '{<Local>{""}</Local>}'
+  )
+
+  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: [
+        {
+          sourceOffsets: [0],
+          generatedOffsets: [51],
+          lengths: [27],
+          data: {
+            completion: true,
+            format: false,
+            navigation: true,
+            semantic: true,
+            structure: true,
+            verification: true
+          }
+        },
+        {
+          sourceOffsets: [28, 38, 56, 58, 71, 73, 88, 99, 111],
+          generatedOffsets: [637, 651, 673, 687, 704, 718, 745, 760, 776],
+          lengths: [9, 17, 2, 12, 2, 15, 10, 11, 21],
+          data: {
+            completion: true,
+            format: false,
+            navigation: true,
+            semantic: true,
+            structure: true,
+            verification: true
+          }
+        }
+      ],
+      snapshot: snapshotFromLines(
+        '/* @jsxRuntime automatic',
+        '@jsxImportSource react */',
+        'export function Local() {}',
+        '',
+        '',
+        '/**',
+        ' * @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) {',
+        '  /**',
+        '   * @internal',
+        '   *   **Do not use.** This is an MDX internal.',
+        '   */',
+        '  const _components = {',
+        '    ...props.components,',
+        '    /** The [props](https://mdxjs.com/docs/using-mdx/#props) that have been passed to the MDX component. */',
+        '    props,',
+        '    /** {@link Local} */',
+        '    Local',
+        '  }',
+        '  return <>',
+        '    {<div />}',
+        '    {<div>{""}</div>}',
+        '    {<_components.Injected />}',
+        '    {<_components.Injected>{""}</_components.Injected>}',
+        '    {<Local />}',
+        '    {<Local>{""}</Local>}',
+        '  </>',
+        '}',
+        '',
+        '/**',
+        ' * 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) & {components?: {}}} MDXContentProps */',
+        ''
+      )
+    },
+    {
+      embeddedCodes: [],
+      id: 'md',
+      languageId: 'markdown',
+      mappings: [
+        {
+          sourceOffsets: [26, 37, 55, 70, 98, 110],
+          generatedOffsets: [0, 9, 17, 25, 33, 41],
+          lengths: [2, 1, 1, 1, 1, 1],
+          data: {
+            completion: true,
+            format: false,
+            navigation: true,
+            semantic: true,
+            structure: true,
+            verification: true
+          }
+        }
+      ],
+      snapshot: snapshotFromLines(
+        '',
+        '',
+        '<!---->',
+        '<!---->',
+        '<!---->',
+        '<!---->',
+        '<!---->',
+        '<!---->'
+      )
+    }
+  ])
+})
+
+test('create virtual code w/ prefixed JSX expressions in attributes', () => {
+  const plugin = createMdxLanguagePlugin()
+
+  const snapshot = snapshotFromLines(
+    'export function Local() {}',
+    '',
+    '<div local={<Local />} injected={<Injected />} string="string" boolean />',
+    '',
+    '<div local={<Local>{null}</Local>} injected={<Injected>{null}</Injected>} string="string" boolean>',
+    '  paragraph',
+    '</div>'
+  )
+
+  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: [
+        {
+          sourceOffsets: [0],
+          generatedOffsets: [51],
+          lengths: [27],
+          data: {
+            completion: true,
+            format: false,
+            navigation: true,
+            semantic: true,
+            structure: true,
+            verification: true
+          }
+        },
+        {
+          sourceOffsets: [28, 62, 103, 149, 166, 214],
+          generatedOffsets: [637, 683, 727, 785, 814, 878],
+          lengths: [34, 39, 46, 17, 35, 6],
+          data: {
+            completion: true,
+            format: false,
+            navigation: true,
+            semantic: true,
+            structure: true,
+            verification: true
+          }
+        }
+      ],
+      snapshot: snapshotFromLines(
+        '/* @jsxRuntime automatic',
+        '@jsxImportSource react */',
+        'export function Local() {}',
+        '',
+        '',
+        '/**',
+        ' * @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) {',
+        '  /**',
+        '   * @internal',
+        '   *   **Do not use.** This is an MDX internal.',
+        '   */',
+        '  const _components = {',
+        '    ...props.components,',
+        '    /** The [props](https://mdxjs.com/docs/using-mdx/#props) that have been passed to the MDX component. */',
+        '    props,',
+        '    /** {@link Local} */',
+        '    Local',
+        '  }',
+        '  return <>',
+        '    <div local={<Local />} injected={<_components.Injected />} string="string" boolean />',
+        '    <div local={<Local>{null}</Local>} injected={<_components.Injected>{null}</_components.Injected>} string="string" boolean>',
+        '    <>',
+        "    {''}",
+        '    </>',
+        '    </div>',
+        '  </>',
+        '}',
+        '',
+        '/**',
+        ' * 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) & {components?: {}}} MDXContentProps */',
+        ''
+      )
+    },
+    {
+      embeddedCodes: [],
+      id: 'md',
+      languageId: 'markdown',
+      mappings: [
+        {
+          sourceOffsets: [26, 101, 201, 204],
+          generatedOffsets: [0, 9, 18, 19],
+          lengths: [2, 2, 1, 10],
+          data: {
+            completion: true,
+            format: false,
+            navigation: true,
+            semantic: true,
+            structure: true,
+            verification: true
+          }
+        }
+      ],
+      snapshot: snapshotFromLines(
+        '',
+        '',
+        '<!---->',
+        '',
+        '<!---->',
+        'paragraph',
+        '<!---->'
+      )
+    }
+  ])
+})
+
 test('create virtual code w/ mdxJsxFlowElement w/ children', () => {
   const plugin = createMdxLanguagePlugin()