From 68aa697f4b8c1febe702de74739d2ffefc3e76b9 Mon Sep 17 00:00:00 2001 From: jp-knj Date: Tue, 17 Mar 2026 22:19:43 +0900 Subject: [PATCH 1/2] wip --- packages/language-service/lib/language-plugin.js | 14 +++++++++++++- packages/language-service/package.json | 3 +++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/packages/language-service/lib/language-plugin.js b/packages/language-service/lib/language-plugin.js index 4a398fbd..5fb695de 100644 --- a/packages/language-service/lib/language-plugin.js +++ b/packages/language-service/lib/language-plugin.js @@ -7,11 +7,21 @@ * @import {VirtualCodePlugin} from './plugins/plugin.js' */ +import {Parser} from 'acorn' +import acornJsx from 'acorn-jsx' +import {LooseParser} from 'acorn-loose' import remarkMdx from 'remark-mdx' import remarkParse from 'remark-parse' import {unified} from 'unified' import {VirtualMdxCode} from './virtual-code.js' +const StrictParser = Parser.extend(acornJsx()) + +const hybridAcorn = { + parse: LooseParser.parse.bind(LooseParser), + parseExpressionAt: StrictParser.parseExpressionAt.bind(StrictParser) +} + /** * Create a [Volar](https://volarjs.dev) language plugin to support MDX. * @@ -32,7 +42,9 @@ export function createMdxLanguagePlugin( checkMdx = false, jsxImportSource = 'react' ) { - const processor = unified().use(remarkParse).use(remarkMdx) + const processor = unified() + .use(remarkParse) + .use(remarkMdx, {acorn: hybridAcorn}) if (remarkPlugins) { processor.use(remarkPlugins) } diff --git a/packages/language-service/package.json b/packages/language-service/package.json index b8378a92..2f1819ef 100644 --- a/packages/language-service/package.json +++ b/packages/language-service/package.json @@ -39,6 +39,9 @@ "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "@volar/language-service": "~2.4.0", + "acorn": "^8.14.0", + "acorn-jsx": "^5.3.0", + "acorn-loose": "^8.5.2", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", From ce0c182fbc090d8433915d5cbe43addb2b3370d6 Mon Sep 17 00:00:00 2001 From: jp-knj Date: Wed, 18 Mar 2026 21:03:05 +0900 Subject: [PATCH 2/2] feat: add tests for acorn-loose ESM recovery and update parse error expectations --- .../language-server/test/diagnostics.test.js | 9 +-- .../language-service/test/language-plugin.js | 57 +++++++++++++++++++ 2 files changed, 62 insertions(+), 4 deletions(-) diff --git a/packages/language-server/test/diagnostics.test.js b/packages/language-server/test/diagnostics.test.js index e39b11e1..44e3032c 100644 --- a/packages/language-server/test/diagnostics.test.js +++ b/packages/language-server/test/diagnostics.test.js @@ -31,9 +31,9 @@ test('parse errors', async () => { kind: 'full', items: [ { - code: 'micromark-extension-mdxjs-esm:acorn', + code: 'micromark-extension-mdxjs-esm:non-esm', codeDescription: { - href: 'https://github.com/micromark/micromark-extension-mdxjs-esm#could-not-parse-importexports-with-acorn' + href: 'https://github.com/micromark/micromark-extension-mdxjs-esm#unexpected-type-in-code-only-importexports-are-supported' }, data: { documentUri: String( @@ -50,10 +50,11 @@ test('parse errors', async () => { uri: fixtureUri('node16/syntax-error.mdx'), version: 0 }, - message: 'Could not parse import/exports with acorn', + message: + 'Unexpected `ExpressionStatement` in code: only import/exports are supported', range: { end: { - character: 7, + character: 10, line: 0 }, start: { diff --git a/packages/language-service/test/language-plugin.js b/packages/language-service/test/language-plugin.js index 8eb9a776..50d30643 100644 --- a/packages/language-service/test/language-plugin.js +++ b/packages/language-service/test/language-plugin.js @@ -5286,6 +5286,63 @@ test('rehype-mdx-title matching rank', () => { ]) }) +test('create virtual code w/ invalid ESM recovery', () => { + const plugin = createMdxLanguagePlugin() + + const snapshot = snapshotFromLines('export const x =', '', 'Hello {true}', '') + + const code = plugin.createVirtualCode?.('/test.mdx', 'mdx', snapshot, { + getAssociatedScript: () => undefined + }) + + assert.ok(code instanceof VirtualMdxCode) + assert.ifError(code.error) + // The key assertion: embedded code has non-empty mappings despite broken ESM. + // Before acorn-loose recovery, this would have empty mappings ([]). + assert.ok(code.embeddedCodes[0].mappings.length > 0) +}) + +test('create virtual code w/ incomplete import recovery', () => { + const plugin = createMdxLanguagePlugin() + + const snapshot = snapshotFromLines('import { } from', '', '# Hello', '') + + const code = plugin.createVirtualCode?.('/test.mdx', 'mdx', snapshot, { + getAssociatedScript: () => undefined + }) + + assert.ok(code instanceof VirtualMdxCode) + assert.ifError(code.error) + assert.ok(code.embeddedCodes[0].mappings.length > 0) +}) + +test('create virtual code w/ valid ESM after recovery support', () => { + const plugin = createMdxLanguagePlugin() + + const snapshot = snapshotFromLines( + 'import {Planet} from "./Planet.js"', + 'export const greeting = "hello"', + '', + 'Hello {greeting}', + '' + ) + + const code = plugin.createVirtualCode?.('/test.mdx', 'mdx', snapshot, { + getAssociatedScript: () => undefined + }) + + assert.ok(code instanceof VirtualMdxCode) + assert.ifError(code.error) + assert.ok(code.embeddedCodes[0].mappings.length > 0) + // Verify the generated JS contains both the import and export + const jsText = code.embeddedCodes[0].snapshot.getText( + 0, + code.embeddedCodes[0].snapshot.getLength() + ) + assert.ok(jsText.includes('import {Planet} from "./Planet.js"')) + assert.ok(jsText.includes('export const greeting = "hello"')) +}) + /** * @param {string[]} lines * @returns {typescript.IScriptSnapshot}