Skip to content

Commit f48e8c2

Browse files
committed
feat(language-service): add language plugins for type checking external components
Add full type checking and IntelliSense support for components imported from Vue, Svelte, and Astro. Changes: - Add `resolveLanguagePlugins` function to load plugins from tsconfig.json - Add framework shorthand support ("vue", "svelte", "astro") for easy configuration - Add runtime validation for LanguagePlugin interface conformance - Return structured errors for failed plugin imports or invalid exports - Publish diagnostics to tsconfig.json when plugins fail to load - Log errors to output channel for visibility - Collect extra file extensions from loaded plugins for file watching - Update typescript-plugin to use new API with error logging - Add `languagePlugins` option to JSON schema for editor autocomplete - Add documentation for all supported frameworks - Add helpful error messages with install commands for missing dependencies
1 parent 4851c2d commit f48e8c2

26 files changed

Lines changed: 4090 additions & 19 deletions

.changeset/ready-dingos-grab.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'@mdx-js/typescript-plugin': minor
3+
'@mdx-js/language-service': minor
4+
'@mdx-js/language-server': minor
5+
'vscode-mdx': minor
6+
---
7+
8+
Add full type checking and IntelliSense support for components imported from
9+
Vue, Svelte, and Astro.
10+
This is an opt-in feature.

README.md

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ This repository contains the code to provide editor tooling support for [MDX][].
1414
* [Use](#use)
1515
* [TypeScript](#typescript)
1616
* [Plugins](#plugins)
17+
* [Language Plugins](#language-plugins)
1718
* [Contribute](#contribute)
1819
* [Sponsor](#sponsor)
1920
* [License](#license)
@@ -174,6 +175,83 @@ For example, to support [frontmatter][] with YAML and TOML and [GFM][]:
174175

175176
For a more complete list, see [remark plugins][].
176177

178+
### Language Plugins
179+
180+
MDX Analyzer supports importing components from other frameworks like Vue,
181+
Svelte, and Astro in your MDX files with full type checking and IntelliSense.
182+
183+
Configure language plugins in `tsconfig.json` using framework shorthands or
184+
custom module specifiers:
185+
186+
```jsonc
187+
{
188+
"compilerOptions": {
189+
//
190+
},
191+
"mdx": {
192+
"languagePlugins": [
193+
"vue", // Framework shorthand
194+
"svelte", // Framework shorthand
195+
"astro", // Framework shorthand
196+
"./my-custom-plugin.js" // Custom plugin (relative path)
197+
]
198+
}
199+
}
200+
```
201+
202+
#### Supported Frameworks
203+
204+
* **`"vue"`** — Import Vue components (requires `@vue/language-core`)
205+
* **`"svelte"`** — Import Svelte components (requires `svelte2tsx`)
206+
* **`"astro"`** — Import Astro components (requires `@astrojs/ts-plugin`)
207+
208+
For built-in frameworks, the analyzer will automatically:
209+
210+
1. Validate that required dependencies are installed
211+
2. Show helpful error messages with install commands if dependencies are missing
212+
3. Provide full IntelliSense and type checking for imported components
213+
214+
You can also provide custom module specifiers (relative paths like `"./my-plugin.js"`
215+
or npm packages like `"@my-org/my-plugin"`) to load your own language plugins.
216+
217+
Custom plugins must export a `getLanguagePlugin()` function that returns a
218+
[Volar LanguagePlugin][volar].
219+
For type checking support, the plugin must include:
220+
221+
* `getLanguageId()` method (required)
222+
* `typescript.getServiceScript()` method (required for type checking, added via
223+
`@volar/typescript`)
224+
225+
#### Example: Vue Components
226+
227+
Install the required peer dependency:
228+
229+
```bash
230+
npm install @vue/language-core
231+
```
232+
233+
Configure in `tsconfig.json`:
234+
235+
```jsonc
236+
{
237+
"mdx": {
238+
"languagePlugins": ["vue"]
239+
}
240+
}
241+
```
242+
243+
Now you can import Vue components in MDX:
244+
245+
```mdx
246+
import Counter from './Counter.vue'
247+
248+
<Counter initialCount={5} />
249+
```
250+
251+
If a language plugin fails to load (missing dependencies, module not found,
252+
etc.), an error will be shown in the `tsconfig.json` file with helpful
253+
instructions.
254+
177255
## Contribute
178256

179257
See [§ Contribute][contribute] on our site for ways to get started.

bun.lock

Lines changed: 2594 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,14 @@
1212
"test": "npm run test-types && npm run format && npm run test-api"
1313
},
1414
"devDependencies": {
15+
"@astrojs/ts-plugin": "^1.10.6",
1516
"@changesets/changelog-github": "^0.5.0",
1617
"@changesets/cli": "^2.0.0",
1718
"@mdx-js/node-loader": "^3.0.0",
1819
"@types/react": "^19.0.0",
1920
"@types/react-dom": "^19.0.0",
21+
"@vue/compiler-sfc": "^3.5.26",
22+
"@vue/language-core": "^3.2.1",
2023
"c8": "^10.0.0",
2124
"prettier": "^3.0.0",
2225
"react": "^19.0.0",
@@ -25,6 +28,7 @@
2528
"remark-directive": "^4.0.0",
2629
"remark-frontmatter": "^5.0.0",
2730
"remark-preset-wooorm": "^11.0.0",
31+
"svelte2tsx": "^0.7.46",
2832
"typescript": "^5.0.0",
2933
"xo": "^1.0.0"
3034
},

packages/language-server/lib/index.js

Lines changed: 81 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
#!/usr/bin/env node
22

33
/**
4-
* @import {VirtualCodePlugin} from '@mdx-js/language-service'
4+
* @import {LanguagePluginLoadError, VirtualCodePlugin} from '@mdx-js/language-service'
5+
* @import {LanguagePlugin} from '@volar/language-core'
6+
* @import {Diagnostic} from 'vscode-languageserver'
57
* @import {PluggableList} from 'unified'
68
*/
79

@@ -12,7 +14,8 @@ import process from 'node:process'
1214
import {
1315
createMdxLanguagePlugin,
1416
createMdxServicePlugin,
15-
resolvePlugins
17+
resolvePlugins,
18+
resolveLanguagePlugins
1619
} from '@mdx-js/language-service'
1720
import {
1821
createConnection,
@@ -25,6 +28,8 @@ import remarkGfm from 'remark-gfm'
2528
import {create as createMarkdownServicePlugin} from 'volar-service-markdown'
2629
import {create as createTypeScriptServicePlugin} from 'volar-service-typescript'
2730
import {create as createTypeScriptSyntacticServicePlugin} from 'volar-service-typescript/lib/plugins/syntactic.js'
31+
import {DiagnosticSeverity} from 'vscode-languageserver'
32+
import {URI} from 'vscode-uri'
2833

2934
process.title = 'mdx-language-server'
3035

@@ -33,6 +38,10 @@ const defaultPlugins = [[remarkFrontmatter, ['toml', 'yaml']], remarkGfm]
3338
const connection = createConnection()
3439
const server = createServer(connection)
3540
let tsEnabled = false
41+
/** @type {string[]} */
42+
const extraExtensions = []
43+
/** @type {Map<string, LanguagePluginLoadError[]>} */
44+
const pluginErrorsByTsconfig = new Map()
3645

3746
connection.onInitialize(async (parameters) => {
3847
const tsdk = parameters.initializationOptions?.typescript?.tsdk
@@ -89,6 +98,9 @@ connection.onInitialize(async (parameters) => {
8998
let checkMdx = false
9099
let jsxImportSource = 'react'
91100

101+
/** @type {LanguagePlugin<URI>[]} */
102+
const languagePlugins = []
103+
92104
if (tsconfig) {
93105
const cwd = path.dirname(tsconfig)
94106
const configSourceFile = typescript.readJsonConfigFile(
@@ -111,16 +123,55 @@ connection.onInitialize(async (parameters) => {
111123
)
112124
checkMdx = Boolean(commandLine.raw?.mdx?.checkMdx)
113125
jsxImportSource = commandLine.options.jsxImportSource || jsxImportSource
126+
127+
// Resolve external language plugins
128+
const {plugins: externalPlugins, errors} = resolveLanguagePlugins(
129+
commandLine.raw?.mdx,
130+
(name) => require(name)
131+
)
132+
133+
if (externalPlugins.length > 0) {
134+
// External plugins are validated at runtime; cast is safe as URI extends string behavior
135+
languagePlugins.push(
136+
.../** @type {LanguagePlugin<URI>[]} */ (externalPlugins)
137+
)
138+
}
139+
140+
// Store errors for diagnostics (will be published in onInitialized)
141+
if (errors.length > 0) {
142+
pluginErrorsByTsconfig.set(tsconfig, errors)
143+
144+
// Also log to console for visibility
145+
for (const error of errors) {
146+
connection.console.error(`[MDX] ${error.message}`)
147+
}
148+
}
114149
}
115150

116-
return [
151+
// Add MDX language plugin
152+
languagePlugins.push(
117153
createMdxLanguagePlugin(
118154
remarkPlugins || defaultPlugins,
119155
virtualCodePlugins,
120156
checkMdx,
121157
jsxImportSource
122158
)
123-
]
159+
)
160+
161+
// Collect extra file extensions from all plugins
162+
for (const plugin of languagePlugins) {
163+
const extraFileExtensions = /** @type {any} */ (plugin).typescript
164+
?.extraFileExtensions
165+
if (Array.isArray(extraFileExtensions)) {
166+
for (const ext of extraFileExtensions) {
167+
if (ext.extension && !extraExtensions.includes(ext.extension)) {
168+
extraExtensions.push(ext.extension)
169+
}
170+
}
171+
}
172+
}
173+
174+
return languagePlugins
124175
}
125176
})
126177

@@ -140,6 +191,32 @@ connection.onInitialized(() => {
140191
)
141192
}
142193

194+
// Update extensions based on loaded plugins
195+
for (const ext of extraExtensions) {
196+
if (!extensions.includes(ext)) {
197+
extensions.push(ext)
198+
}
199+
}
200+
201+
// Publish diagnostics for any language plugin loading errors
202+
for (const [tsconfig, errors] of pluginErrorsByTsconfig) {
203+
/** @type {Diagnostic[]} */
204+
const diagnostics = errors.map((error) => ({
205+
severity: DiagnosticSeverity.Error,
206+
range: {
207+
start: {line: 0, character: 0},
208+
end: {line: 0, character: 0}
209+
},
210+
message: error.message,
211+
source: 'mdx'
212+
}))
213+
214+
connection.sendDiagnostics({
215+
uri: URI.file(tsconfig).toString(),
216+
diagnostics
217+
})
218+
}
219+
143220
server.initialized()
144221
server.fileWatcher.watchFiles([`**/*.{${extensions.join(',')}}`])
145222
})

packages/language-server/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@
3636
"remark-frontmatter": "^5.0.0",
3737
"remark-gfm": "^4.0.0",
3838
"volar-service-markdown": "0.0.65",
39-
"volar-service-typescript": "0.0.65"
39+
"volar-service-typescript": "0.0.65",
40+
"vscode-languageserver": "^9.0.1"
4041
},
4142
"devDependencies": {
4243
"@types/node": "^22.0.0",
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
/**
22
* @typedef {import('./plugins/plugin.js').VirtualCodePlugin} VirtualCodePlugin
3+
* @typedef {import('./language-plugins/resolver.js').LanguagePluginLoadError} LanguagePluginLoadError
4+
* @typedef {import('./language-plugins/resolver.js').ResolveLanguagePluginsResult} ResolveLanguagePluginsResult
35
*/
46

57
export {createMdxLanguagePlugin} from './language-plugin.js'
68
export {createMdxServicePlugin} from './service-plugin.js'
7-
export {resolvePlugins} from './tsconfig.js'
9+
export {resolvePlugins, resolveLanguagePlugins} from './tsconfig.js'

0 commit comments

Comments
 (0)