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

Issue:
Support MDX provider
### 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

#260 adds support for the `components` prop. Another common way to inject custom components is using an MDX provider.

### Solution

Let the user define which components are provided via a reserved name, let’s call it `MDXProvidedComponents`. Much like `Props`, the language server assumes it will be available with some fallback logic. That means a user can write the following code to have the provided components available on a per-file basis. This is needed if multiple MDX providers are used.

```ts
// types.ts
import { ReactNode } from 'react'

interface PlanetProps {
  name: string
}

export interface MDXProvidedComponents {
  Planet: (props: PlanetProps) => ReactNode
}
```

```mdx
# MDX Document

{/** @typedef {import('./types.js').MDXProvidedComponents} MDXProvidedComponents */}

<Planet name="Saturn" />
```

TypeScript also supports global types. This means that alternatively, the user can define the type globally and have it available in every document. This is the ideal solution if there’s only one MDX provider in the entire tree.

```ts
// types.ts
import { ReactNode } from 'react'

interface PlanetProps {
  name: string
}

declare global {
  interface MDXProvidedComponents {
    Planet: (props: PlanetProps) => ReactNode
  }
}
```

```mdx
# MDX Document

<Planet name="Saturn" />
```

### Alternatives

I looked into augmenting `@types/mdx`, but I don’t think that’s great. The `MDXComponents` is very loose, allows injecting any key. This is great for the developer DX we’re providing without this more advanced MDX language tooling, but we can do better if the user uses the MDX language server. I.e. it reduces the usefulness of autocomplete and validation.

Pull request description:
Support custom components provided via a provider
### 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 adds support for components provided via the `providerImportSource` option.

The user can specify which components are available by specifying the `MDXProvidedComponents` type either locally inside the MDX file or for all MDX files by declaring it globally.

Closes #399

<!--do not edit: pr-->


Patch:
diff --git a/.changeset/quiet-beds-attend.md b/.changeset/quiet-beds-attend.md
new file mode 100644
index 0000000..3caf98f
--- /dev/null
+++ b/.changeset/quiet-beds-attend.md
@@ -0,0 +1,7 @@
+---
+"@mdx-js/language-service": patch
+"@mdx-js/language-server": patch
+"vscode-mdx": patch
+---
+
+Support custom components provided via a provider
diff --git a/fixtures/fixtures.code-workspace b/fixtures/fixtures.code-workspace
index f76bdc2..02716af 100644
--- a/fixtures/fixtures.code-workspace
+++ b/fixtures/fixtures.code-workspace
@@ -11,6 +11,7 @@
     {"path": "demo"},
     {"path": "frontmatter"},
     {"path": "no-tsconfig"},
-    {"path": "node16"}
+    {"path": "node16"},
+    {"path": "provide"}
   ]
 }
diff --git a/fixtures/provide/components.tsx b/fixtures/provide/components.tsx
new file mode 100644
index 0000000..72fde99
--- /dev/null
+++ b/fixtures/provide/components.tsx
@@ -0,0 +1,31 @@
+import {type ReactNode} from 'react'
+
+export type PlanetProps = {
+  distanceFromStar: number
+
+  name: string
+
+  radius: number
+}
+
+/**
+ * A planet in a solar system.
+ */
+export function Planet({
+  distanceFromStar,
+  name,
+  radius
+}: PlanetProps): ReactNode {
+  return (
+    <section>
+      <dl>
+        <dd>Name</dd>
+        <dt>{name}</dt>
+        <dd>Radius</dd>
+        <dt>{radius}</dt>
+        <dd>Distance from star</dd>
+        <dt>{distanceFromStar}</dt>
+      </dl>
+    </section>
+  )
+}
diff --git a/fixtures/provide/solar-system.mdx b/fixtures/provide/solar-system.mdx
new file mode 100644
index 0000000..53617ac
--- /dev/null
+++ b/fixtures/provide/solar-system.mdx
@@ -0,0 +1,3 @@
+# Solar system
+
+<Planet name="Earth" radius={6371} />
diff --git a/fixtures/provide/tsconfig.json b/fixtures/provide/tsconfig.json
new file mode 100644
index 0000000..c839785
--- /dev/null
+++ b/fixtures/provide/tsconfig.json
@@ -0,0 +1,10 @@
+{
+  "compilerOptions": {
+    "jsx": "react-jsx",
+    "module": "node16",
+    "strict": true
+  },
+  "mdx": {
+    "checkMdx": true
+  }
+}
diff --git a/fixtures/provide/types.ts b/fixtures/provide/types.ts
new file mode 100644
index 0000000..34c80b9
--- /dev/null
+++ b/fixtures/provide/types.ts
@@ -0,0 +1,8 @@
+import {type Planet} from './components.js'
+
+declare global {
+  // eslint-disable-next-line @typescript-eslint/naming-convention
+  type MDXProvidedComponents = {
+    Planet: typeof Planet
+  }
+}
diff --git a/packages/language-service/lib/virtual-code.js b/packages/language-service/lib/virtual-code.js
index 2fd750b..518d85e 100644
--- a/packages/language-service/lib/virtual-code.js
+++ b/packages/language-service/lib/virtual-code.js
@@ -69,6 +69,8 @@ ${isAsync ? 'async ' : ''}function _createMdxContent(props) {
    *   **Do not use.** This is an MDX internal.
    */
   const _components = {
+    // @ts-ignore
+    .../** @type {0 extends 1 & MDXProvidedComponents ? {} : MDXProvidedComponents} */ ({}),
     ...props.components,
     /** The [props](https://mdxjs.com/docs/using-mdx/#props) that have been passed to the MDX component. */
     props${Array.from(variables, (name) => ',\n    /** {@link ' + name + '} */\n    ' + name).join('')}


Test patch:
diff --git a/packages/language-server/test/diagnostics.test.js b/packages/language-server/test/diagnostics.test.js
index 470ecda..de5548d 100644
--- a/packages/language-server/test/diagnostics.test.js
+++ b/packages/language-server/test/diagnostics.test.js
@@ -135,3 +135,48 @@ test('does not resolve shadow content', async () => {
     kind: 'full'
   })
 })
+
+test('provided components', async () => {
+  const {uri} = await serverHandle.openTextDocument(
+    fixturePath('provide/solar-system.mdx'),
+    'mdx'
+  )
+  const diagnostics = await serverHandle.sendDocumentDiagnosticRequest(uri)
+
+  assert.deepEqual(diagnostics, {
+    items: [
+      {
+        code: 2741,
+        data: {
+          documentUri: fixtureUri('provide/solar-system.mdx?virtualCodeId=jsx'),
+          isFormat: false,
+          original: {},
+          serviceIndex: 2,
+          uri: fixtureUri('provide/solar-system.mdx'),
+          version: 1
+        },
+        message:
+          "Property 'distanceFromStar' is missing in type '{ name: string; radius: number; }' but required in type 'PlanetProps'.",
+        range: {
+          end: {character: 7, line: 2},
+          start: {character: 1, line: 2}
+        },
+        relatedInformation: [
+          {
+            location: {
+              range: {
+                end: {character: 18, line: 3},
+                start: {character: 2, line: 3}
+              },
+              uri: fixtureUri('provide/components.tsx')
+            },
+            message: "'distanceFromStar' is declared here."
+          }
+        ],
+        severity: 1,
+        source: 'ts'
+      }
+    ],
+    kind: 'full'
+  })
+})
diff --git a/packages/language-service/test/language-module.js b/packages/language-service/test/language-module.js
index 1048ddb..be33337 100644
--- a/packages/language-service/test/language-module.js
+++ b/packages/language-service/test/language-module.js
@@ -76,6 +76,8 @@ test('create virtual code w/ mdxjsEsm', () => {
         '   *   **Do not use.** This is an MDX internal.',
         '   */',
         '  const _components = {',
+        '    // @ts-ignore',
+        '    .../** @type {0 extends 1 & MDXProvidedComponents ? {} : MDXProvidedComponents} */ ({}),',
         '    ...props.components,',
         '    /** The [props](https://mdxjs.com/docs/using-mdx/#props) that have been passed to the MDX component. */',
         '    props,',
@@ -189,6 +191,8 @@ test('create virtual code w/o MDX layout in case of named re-export', () => {
         '   *   **Do not use.** This is an MDX internal.',
         '   */',
         '  const _components = {',
+        '    // @ts-ignore',
+        '    .../** @type {0 extends 1 & MDXProvidedComponents ? {} : MDXProvidedComponents} */ ({}),',
         '    ...props.components,',
         '    /** The [props](https://mdxjs.com/docs/using-mdx/#props) that have been passed to the MDX component. */',
         '    props',
@@ -301,6 +305,8 @@ test('create virtual code w/ MDX layout in case of default re-export', () => {
         '   *   **Do not use.** This is an MDX internal.',
         '   */',
         '  const _components = {',
+        '    // @ts-ignore',
+        '    .../** @type {0 extends 1 & MDXProvidedComponents ? {} : MDXProvidedComponents} */ ({}),',
         '    ...props.components,',
         '    /** The [props](https://mdxjs.com/docs/using-mdx/#props) that have been passed to the MDX component. */',
         '    props',
@@ -416,6 +422,8 @@ test('create virtual code w/ MDX layout in case of named and default re-export',
         '   *   **Do not use.** This is an MDX internal.',
         '   */',
         '  const _components = {',
+        '    // @ts-ignore',
+        '    .../** @type {0 extends 1 & MDXProvidedComponents ? {} : MDXProvidedComponents} */ ({}),',
         '    ...props.components,',
         '    /** The [props](https://mdxjs.com/docs/using-mdx/#props) that have been passed to the MDX component. */',
         '    props',
@@ -531,6 +539,8 @@ test('create virtual code w/ MDX layout in case of default and named re-export',
         '   *   **Do not use.** This is an MDX internal.',
         '   */',
         '  const _components = {',
+        '    // @ts-ignore',
+        '    .../** @type {0 extends 1 & MDXProvidedComponents ? {} : MDXProvidedComponents} */ ({}),',
         '    ...props.components,',
         '    /** The [props](https://mdxjs.com/docs/using-mdx/#props) that have been passed to the MDX component. */',
         '    props',
@@ -656,6 +666,8 @@ test('create virtual code w/ MDX layout in case of a default exported arrow func
         '   *   **Do not use.** This is an MDX internal.',
         '   */',
         '  const _components = {',
+        '    // @ts-ignore',
+        '    .../** @type {0 extends 1 & MDXProvidedComponents ? {} : MDXProvidedComponents} */ ({}),',
         '    ...props.components,',
         '    /** The [props](https://mdxjs.com/docs/using-mdx/#props) that have been passed to the MDX component. */',
         '    props',
@@ -784,6 +796,8 @@ test('create virtual code w/ MDX layout in case of a default exported function d
         '   *   **Do not use.** This is an MDX internal.',
         '   */',
         '  const _components = {',
+        '    // @ts-ignore',
+        '    .../** @type {0 extends 1 & MDXProvidedComponents ? {} : MDXProvidedComponents} */ ({}),',
         '    ...props.components,',
         '    /** The [props](https://mdxjs.com/docs/using-mdx/#props) that have been passed to the MDX component. */',
         '    props,',
@@ -898,6 +912,8 @@ test('create virtual code w/ MDX layout in case of a default exported constant',
         '   *   **Do not use.** This is an MDX internal.',
         '   */',
         '  const _components = {',
+        '    // @ts-ignore',
+        '    .../** @type {0 extends 1 & MDXProvidedComponents ? {} : MDXProvidedComponents} */ ({}),',
         '    ...props.components,',
         '    /** The [props](https://mdxjs.com/docs/using-mdx/#props) that have been passed to the MDX component. */',
         '    props',
@@ -1026,6 +1042,8 @@ test('create virtual code w/ MDX layout and matching argument name', () => {
         '   *   **Do not use.** This is an MDX internal.',
         '   */',
         '  const _components = {',
+        '    // @ts-ignore',
+        '    .../** @type {0 extends 1 & MDXProvidedComponents ? {} : MDXProvidedComponents} */ ({}),',
         '    ...props.components,',
         '    /** The [props](https://mdxjs.com/docs/using-mdx/#props) that have been passed to the MDX component. */',
         '    props,',
@@ -1158,6 +1176,8 @@ test('create virtual code w/ MDX layout in case of a default export followed by
         '   *   **Do not use.** This is an MDX internal.',
         '   */',
         '  const _components = {',
+        '    // @ts-ignore',
+        '    .../** @type {0 extends 1 & MDXProvidedComponents ? {} : MDXProvidedComponents} */ ({}),',
         '    ...props.components,',
         '    /** The [props](https://mdxjs.com/docs/using-mdx/#props) that have been passed to the MDX component. */',
         '    props,',
@@ -1292,6 +1312,8 @@ test('create virtual code w/ MDX layout in case of a default export preceded by
         '   *   **Do not use.** This is an MDX internal.',
         '   */',
         '  const _components = {',
+        '    // @ts-ignore',
+        '    .../** @type {0 extends 1 & MDXProvidedComponents ? {} : MDXProvidedComponents} */ ({}),',
         '    ...props.components,',
         '    /** The [props](https://mdxjs.com/docs/using-mdx/#props) that have been passed to the MDX component. */',
         '    props,',
@@ -1376,7 +1398,7 @@ test('create virtual code w/ mdxFlowExpression', () => {
       mappings: [
         {
           sourceOffsets: [0],
-          generatedOffsets: [573],
+          generatedOffsets: [684],
           lengths: [9],
           data: {
             completion: true,
@@ -1405,6 +1427,8 @@ test('create virtual code w/ mdxFlowExpression', () => {
         '   *   **Do not use.** This is an MDX internal.',
         '   */',
         '  const _components = {',
+        '    // @ts-ignore',
+        '    .../** @type {0 extends 1 & MDXProvidedComponents ? {} : MDXProvidedComponents} */ ({}),',
         '    ...props.components,',
         '    /** The [props](https://mdxjs.com/docs/using-mdx/#props) that have been passed to the MDX component. */',
         '    props',
@@ -1508,7 +1532,7 @@ test('create virtual code w/ prefixed JSX expressions for mdxFlowExpression', ()
         },
         {
           sourceOffsets: [28, 38, 56, 58, 71, 73, 88, 99, 111],
-          generatedOffsets: [637, 651, 673, 687, 704, 718, 745, 760, 776],
+          generatedOffsets: [748, 762, 784, 798, 815, 829, 856, 871, 887],
           lengths: [9, 17, 2, 12, 2, 15, 10, 11, 21],
           data: {
             completion: true,
@@ -1539,6 +1563,8 @@ test('create virtual code w/ prefixed JSX expressions for mdxFlowExpression', ()
         '   *   **Do not use.** This is an MDX internal.',
         '   */',
         '  const _components = {',
+        '    // @ts-ignore',
+        '    .../** @type {0 extends 1 & MDXProvidedComponents ? {} : MDXProvidedComponents} */ ({}),',
         '    ...props.components,',
         '    /** The [props](https://mdxjs.com/docs/using-mdx/#props) that have been passed to the MDX component. */',
         '    props,',
@@ -1657,7 +1683,7 @@ test('create virtual code w/ prefixed JSX expressions in attributes', () => {
         },
         {
           sourceOffsets: [28, 62, 103, 149, 166, 214],
-          generatedOffsets: [637, 683, 727, 785, 814, 878],
+          generatedOffsets: [748, 794, 838, 896, 925, 989],
           lengths: [34, 39, 46, 17, 35, 6],
           data: {
             completion: true,
@@ -1688,6 +1714,8 @@ test('create virtual code w/ prefixed JSX expressions in attributes', () => {
         '   *   **Do not use.** This is an MDX internal.',
         '   */',
         '  const _components = {',
+        '    // @ts-ignore',
+        '    .../** @type {0 extends 1 & MDXProvidedComponents ? {} : MDXProvidedComponents} */ ({}),',
         '    ...props.components,',
         '    /** The [props](https://mdxjs.com/docs/using-mdx/#props) that have been passed to the MDX component. */',
         '    props,',
@@ -1816,7 +1844,7 @@ test('create virtual code w/ mdxJsxFlowElement w/ children', () => {
         },
         {
           sourceOffsets: [28, 87, 94, 95, 158, 160, 170, 231],
-          generatedOffsets: [637, 671, 682, 695, 733, 747, 761, 797],
+          generatedOffsets: [748, 782, 793, 806, 844, 858, 872, 908],
           lengths: [5, 6, 1, 9, 2, 9, 7, 8],
           data: {
             completion: true,
@@ -1847,6 +1875,8 @@ test('create virtual code w/ mdxJsxFlowElement w/ children', () => {
         '   *   **Do not use.** This is an MDX internal.',
         '   */',
         '  const _components = {',
+        '    // @ts-ignore',
+        '    .../** @type {0 extends 1 & MDXProvidedComponents ? {} : MDXProvidedComponents} */ ({}),',
         '    ...props.components,',
         '    /** The [props](https://mdxjs.com/docs/using-mdx/#props) that have been passed to the MDX component. */',
         '    props,',
@@ -1983,7 +2013,7 @@ test('create virtual code w/ mdxJsxFlowElement w/o children', () => {
         },
         {
           sourceOffsets: [28, 36, 37, 49],
-          generatedOffsets: [637, 649, 662, 678],
+          generatedOffsets: [748, 760, 773, 789],
           lengths: [7, 1, 11, 9],
           data: {
             completion: true,
@@ -2014,6 +2044,8 @@ test('create virtual code w/ mdxJsxFlowElement w/o children', () => {
         '   *   **Do not use.** This is an MDX internal.',
         '   */',
         '  const _components = {',
+        '    // @ts-ignore',
+        '    .../** @type {0 extends 1 & MDXProvidedComponents ? {} : MDXProvidedComponents} */ ({}),',
         '    ...props.components,',
         '    /** The [props](https://mdxjs.com/docs/using-mdx/#props) that have been passed to the MDX component. */',
         '    props,',
@@ -2119,7 +2151,7 @@ test('create virtual code w/ mdxJsxTextElement', () => {
         },
         {
           sourceOffsets: [30, 41, 42, 56],
-          generatedOffsets: [653, 674, 687, 712],
+          generatedOffsets: [764, 785, 798, 823],
           lengths: [7, 1, 11, 9],
           data: {
             completion: true,
@@ -2150,6 +2182,8 @@ test('create virtual code w/ mdxJsxTextElement', () => {
         '   *   **Do not use.** This is an MDX internal.',
         '   */',
         '  const _components = {',
+        '    // @ts-ignore',
+        '    .../** @type {0 extends 1 & MDXProvidedComponents ? {} : MDXProvidedComponents} */ ({}),',
         '    ...props.components,',
         '    /** The [props](https://mdxjs.com/docs/using-mdx/#props) that have been passed to the MDX component. */',
         '    props,',
@@ -2246,7 +2280,7 @@ test('create virtual code w/ mdxTextExpression', () => {
       languageId: 'javascriptreact',
       mappings: [
         {
-          generatedOffsets: [589],
+          generatedOffsets: [700],
           sourceOffsets: [4],
           lengths: [9],
           data: {
@@ -2276,6 +2310,8 @@ test('create virtual code w/ mdxTextExpression', () => {
         '   *   **Do not use.** This is an MDX internal.',
         '   */',
         '  const _components = {',
+        '    // @ts-ignore',
+        '    .../** @type {0 extends 1 & MDXProvidedComponents ? {} : MDXProvidedComponents} */ ({}),',
         '    ...props.components,',
         '    /** The [props](https://mdxjs.com/docs/using-mdx/#props) that have been passed to the MDX component. */',
         '    props',
@@ -2363,7 +2399,7 @@ test('create virtual code w/ async mdxTextExpression', () => {
       languageId: 'javascriptreact',
       mappings: [
         {
-          generatedOffsets: [595],
+          generatedOffsets: [706],
           sourceOffsets: [4],
           lengths: [32],
           data: {
@@ -2393,6 +2429,8 @@ test('create virtual code w/ async mdxTextExpression', () => {
         '   *   **Do not use.** This is an MDX internal.',
         '   */',
         '  const _components = {',
+        '    // @ts-ignore',
+        '    .../** @type {0 extends 1 & MDXProvidedComponents ? {} : MDXProvidedComponents} */ ({}),',
         '    ...props.components,',
         '    /** The [props](https://mdxjs.com/docs/using-mdx/#props) that have been passed to the MDX component. */',
         '    props',
@@ -2499,6 +2537,8 @@ test('create virtual code w/ dedented markdown content', () => {
         '   *   **Do not use.** This is an MDX internal.',
         '   */',
         '  const _components = {',
+        '    // @ts-ignore',
+        '    .../** @type {0 extends 1 & MDXProvidedComponents ? {} : MDXProvidedComponents} */ ({}),',
         '    ...props.components,',
         '    /** The [props](https://mdxjs.com/docs/using-mdx/#props) that have been passed to the MDX component. */',
         '    props',
@@ -2603,6 +2643,8 @@ test('create virtual code w/ syntax error', () => {
         '   *   **Do not use.** This is an MDX internal.',
         '   */',
         '  const _components = {',
+        '    // @ts-ignore',
+        '    .../** @type {0 extends 1 & MDXProvidedComponents ? {} : MDXProvidedComponents} */ ({}),',
         '    ...props.components,',
         '    /** The [props](https://mdxjs.com/docs/using-mdx/#props) that have been passed to the MDX component. */',
         '    props',
@@ -2684,6 +2726,8 @@ test('create virtual code w/ yaml frontmatter', () => {
         '   *   **Do not use.** This is an MDX internal.',
         '   */',
         '  const _components = {',
+        '    // @ts-ignore',
+        '    .../** @type {0 extends 1 & MDXProvidedComponents ? {} : MDXProvidedComponents} */ ({}),',
         '    ...props.components,',
         '    /** The [props](https://mdxjs.com/docs/using-mdx/#props) that have been passed to the MDX component. */',
         '    props',
@@ -2805,6 +2849,8 @@ test('update virtual code', () => {
         '   *   **Do not use.** This is an MDX internal.',
         '   */',
         '  const _components = {',
+        '    // @ts-ignore',
+        '    .../** @type {0 extends 1 & MDXProvidedComponents ? {} : MDXProvidedComponents} */ ({}),',
         '    ...props.components,',
         '    /** The [props](https://mdxjs.com/docs/using-mdx/#props) that have been passed to the MDX component. */',
         '    props',
@@ -2944,6 +2990,8 @@ test('support checkMdx', () => {
         '   *   **Do not use.** This is an MDX internal.',
         '   */',
         '  const _components = {',
+        '    // @ts-ignore',
+        '    .../** @type {0 extends 1 & MDXProvidedComponents ? {} : MDXProvidedComponents} */ ({}),',
         '    ...props.components,',
         '    /** The [props](https://mdxjs.com/docs/using-mdx/#props) that have been passed to the MDX component. */',
         '    props',
@@ -3039,6 +3087,8 @@ test('support custom jsxImportSource', () => {
         '   *   **Do not use.** This is an MDX internal.',
         '   */',
         '  const _components = {',
+        '    // @ts-ignore',
+        '    .../** @type {0 extends 1 & MDXProvidedComponents ? {} : MDXProvidedComponents} */ ({}),',
         '    ...props.components,',
         '    /** The [props](https://mdxjs.com/docs/using-mdx/#props) that have been passed to the MDX component. */',
         '    props',