diff --git a/packages/language-server/test/diagnostics.test.js b/packages/language-server/test/diagnostics.test.js index b1da05de..76d51957 100644 --- a/packages/language-server/test/diagnostics.test.js +++ b/packages/language-server/test/diagnostics.test.js @@ -79,7 +79,7 @@ test('type errors', async () => { version: 1 }, message: - "Property 'counter' may not exist on type '{ readonly count: number; }'. Did you mean 'count'?", + "Property 'counter' may not exist on type '{ readonly count: number; readonly components?: {}; }'. Did you mean 'count'?", range: { start: {line: 14, character: 51}, end: {line: 14, character: 58} diff --git a/packages/language-service/lib/jsx-utils.js b/packages/language-service/lib/jsx-utils.js new file mode 100644 index 00000000..c3c1bb62 --- /dev/null +++ b/packages/language-service/lib/jsx-utils.js @@ -0,0 +1,25 @@ +/** + * Check if a name belongs to a JSX component that can be injected. + * + * These are components whose name start with an upper case character. They may + * also not be defined in the scope. + * + * @param {string | null} name + * The name of the component to check. + * @param {string[]} scope + * The variable names available in the scope. + * @returns {boolean} + * Whether or not the given name is that of an injectable JSX component. + */ +export function isInjectableComponent(name, scope) { + if (!name) { + return false + } + + const char = name.charAt(0) + if (char !== char.toUpperCase()) { + return false + } + + return !scope.includes(name) +} diff --git a/packages/language-service/lib/virtual-code.js b/packages/language-service/lib/virtual-code.js index 4e7c34d2..0f70fa64 100644 --- a/packages/language-service/lib/virtual-code.js +++ b/packages/language-service/lib/virtual-code.js @@ -12,8 +12,10 @@ */ import {walk} from 'estree-walker' +import {analyze} from 'periscopic' import {getNodeEndOffset, getNodeStartOffset} from './mdast-utils.js' import {ScriptSnapshot} from './script-snapshot.js' +import {isInjectableComponent} from './jsx-utils.js' /** * Render the content that should be prefixed to the embedded JavaScript file. @@ -51,8 +53,9 @@ const layoutJsDoc = (propsName) => ` /** * @param {boolean} isAsync * Whether or not the `_createMdxContent` should be async + * @param {string[]} variables */ -const componentStart = (isAsync) => ` +const componentStart = (isAsync, variables) => ` /** * @deprecated * Do not use. @@ -61,6 +64,15 @@ const componentStart = (isAsync) => ` * The [props](https://mdxjs.com/docs/using-mdx/#props) that have been passed to the MDX component. */ ${isAsync ? 'async ' : ''}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${Array.from(variables, (name) => ',\n /** {@link ' + name + '} */\n ' + name).join('')} + } return ` const componentEnd = ` @@ -77,11 +89,11 @@ export default function MDXContent(props) { } // @ts-ignore -/** @typedef {0 extends 1 & Props ? {} : Props} MDXContentProps */ +/** @typedef {(0 extends 1 & Props ? {} : Props) & {components?: {}}} MDXContentProps */ ` const fallback = - jsPrefix(false, 'react') + componentStart(false) + '<>>' + componentEnd + jsPrefix(false, 'react') + componentStart(false, []) + '<>>' + componentEnd /** * Visit an mdast tree with and enter and exit callback. @@ -379,6 +391,29 @@ function getEmbeddedCodes(mdx, ast, checkMdx, jsxImportSource) { let markdown = '' let nextMarkdownSourceStart = 0 + /** @type {Program} */ + const esmProgram = { + type: 'Program', + sourceType: 'module', + start: 0, + end: 0, + body: [] + } + + for (const child of ast.children) { + if (child.type !== 'mdxjsEsm') { + continue + } + + const estree = child.data?.estree + + if (estree) { + esmProgram.body.push(...estree.body) + } + } + + const variables = [...analyze(esmProgram).scope.declarations.keys()].sort() + /** * Update the **markdown** mappings from a start and end offset of a **JavaScript** chunk. * @@ -487,7 +522,20 @@ function getEmbeddedCodes(mdx, ast, checkMdx, jsxImportSource) { } updateMarkdownFromOffsets(start, end) - jsx = addOffset(jsxMapping, mdx, jsx, start, end) + if (isInjectableComponent(node.name, variables)) { + const openingStart = start + 1 + jsx = addOffset( + jsxMapping, + mdx, + addOffset(jsxMapping, mdx, jsx, start, openingStart) + + '_components.', + openingStart, + end + ) + } else { + jsx = addOffset(jsxMapping, mdx, jsx, start, end) + } + break } @@ -533,7 +581,19 @@ function getEmbeddedCodes(mdx, ast, checkMdx, jsxImportSource) { const end = getNodeEndOffset(node) updateMarkdownFromOffsets(start, end) - jsx = addOffset(jsxMapping, mdx, jsx, start, end) + if (isInjectableComponent(node.name, variables)) { + const closingStart = start + 2 + jsx = addOffset( + jsxMapping, + mdx, + addOffset(jsxMapping, mdx, jsx, start, closingStart) + + '_components.', + closingStart, + end + ) + } else { + jsx = addOffset(jsxMapping, mdx, jsx, start, end) + } } break @@ -557,7 +617,7 @@ function getEmbeddedCodes(mdx, ast, checkMdx, jsxImportSource) { ) updateMarkdownFromOffsets(mdx.length, mdx.length) - esm += componentStart(hasAwait) + esm += componentStart(hasAwait, variables) for (let i = 0; i < jsxMapping.generatedOffsets.length; i++) { jsxMapping.generatedOffsets[i] += esm.length diff --git a/packages/language-service/package.json b/packages/language-service/package.json index 9c9338bb..a91ba7f8 100644 --- a/packages/language-service/package.json +++ b/packages/language-service/package.json @@ -38,6 +38,7 @@ "estree-walker": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", + "periscopic": "^3.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "unified": "^11.0.0", diff --git a/packages/language-service/test/language-module.js b/packages/language-service/test/language-module.js index 716cbc3c..6b6ab0fc 100644 --- a/packages/language-service/test/language-module.js +++ b/packages/language-service/test/language-module.js @@ -72,6 +72,17 @@ test('create virtual code w/ mdxjsEsm', () => { ' * 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 Planet} */', + ' Planet', + ' }', ' return <>>', '}', '', @@ -86,7 +97,7 @@ test('create virtual code w/ mdxjsEsm', () => { '}', '', '// @ts-ignore', - '/** @typedef {0 extends 1 & Props ? {} : Props} MDXContentProps */', + '/** @typedef {(0 extends 1 & Props ? {} : Props) & {components?: {}}} MDXContentProps */', '' ) }, @@ -175,6 +186,15 @@ test('create virtual code w/o MDX layout in case of named re-export', () => { ' * 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', + ' }', ' return <>>', '}', '', @@ -189,7 +209,7 @@ test('create virtual code w/o MDX layout in case of named re-export', () => { '}', '', '// @ts-ignore', - '/** @typedef {0 extends 1 & Props ? {} : Props} MDXContentProps */', + '/** @typedef {(0 extends 1 & Props ? {} : Props) & {components?: {}}} MDXContentProps */', '' ) }, @@ -279,6 +299,15 @@ test('create virtual code w/ MDX layout in case of default re-export', () => { ' * 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', + ' }', ' return <>>', '}', '', @@ -293,7 +322,7 @@ test('create virtual code w/ MDX layout in case of default re-export', () => { '}', '', '// @ts-ignore', - '/** @typedef {0 extends 1 & Props ? {} : Props} MDXContentProps */', + '/** @typedef {(0 extends 1 & Props ? {} : Props) & {components?: {}}} MDXContentProps */', '' ) }, @@ -386,6 +415,15 @@ test('create virtual code w/ MDX layout in case of named and default re-export', ' * 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', + ' }', ' return <>>', '}', '', @@ -400,7 +438,7 @@ test('create virtual code w/ MDX layout in case of named and default re-export', '}', '', '// @ts-ignore', - '/** @typedef {0 extends 1 & Props ? {} : Props} MDXContentProps */', + '/** @typedef {(0 extends 1 & Props ? {} : Props) & {components?: {}}} MDXContentProps */', '' ) }, @@ -493,6 +531,15 @@ test('create virtual code w/ MDX layout in case of default and named re-export', ' * 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', + ' }', ' return <>>', '}', '', @@ -507,7 +554,7 @@ test('create virtual code w/ MDX layout in case of default and named re-export', '}', '', '// @ts-ignore', - '/** @typedef {0 extends 1 & Props ? {} : Props} MDXContentProps */', + '/** @typedef {(0 extends 1 & Props ? {} : Props) & {components?: {}}} MDXContentProps */', '' ) }, @@ -610,6 +657,15 @@ test('create virtual code w/ MDX layout in case of a default exported arrow func ' * 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', + ' }', ' return <>>', '}', '', @@ -624,7 +680,7 @@ test('create virtual code w/ MDX layout in case of a default exported arrow func '}', '', '// @ts-ignore', - '/** @typedef {0 extends 1 & Props ? {} : Props} MDXContentProps */', + '/** @typedef {(0 extends 1 & Props ? {} : Props) & {components?: {}}} MDXContentProps */', '' ) }, @@ -730,6 +786,17 @@ test('create virtual code w/ MDX layout in case of a default exported function d ' * 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 MDXLayout} */', + ' MDXLayout', + ' }', ' return <>>', '}', '', @@ -744,7 +811,7 @@ test('create virtual code w/ MDX layout in case of a default exported function d '}', '', '// @ts-ignore', - '/** @typedef {0 extends 1 & Props ? {} : Props} MDXContentProps */', + '/** @typedef {(0 extends 1 & Props ? {} : Props) & {components?: {}}} MDXContentProps */', '' ) }, @@ -834,6 +901,15 @@ test('create virtual code w/ MDX layout in case of a default exported constant', ' * 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', + ' }', ' return <>>', '}', '', @@ -848,7 +924,7 @@ test('create virtual code w/ MDX layout in case of a default exported constant', '}', '', '// @ts-ignore', - '/** @typedef {0 extends 1 & Props ? {} : Props} MDXContentProps */', + '/** @typedef {(0 extends 1 & Props ? {} : Props) & {components?: {}}} MDXContentProps */', '' ) }, @@ -954,6 +1030,17 @@ test('create virtual code w/ MDX layout and matching argument name', () => { ' * 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 MDXLayout} */', + ' MDXLayout', + ' }', ' return <>>', '}', '', @@ -968,7 +1055,7 @@ test('create virtual code w/ MDX layout and matching argument name', () => { '}', '', '// @ts-ignore', - '/** @typedef {0 extends 1 & Props ? {} : Props} MDXContentProps */', + '/** @typedef {(0 extends 1 & Props ? {} : Props) & {components?: {}}} MDXContentProps */', '' ) }, @@ -1076,6 +1163,19 @@ test('create virtual code w/ MDX layout in case of a default export followed by ' * 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 MDXLayout} */', + ' MDXLayout,', + ' /** {@link named} */', + ' named', + ' }', ' return <>>', '}', '', @@ -1090,7 +1190,7 @@ test('create virtual code w/ MDX layout in case of a default export followed by '}', '', '// @ts-ignore', - '/** @typedef {0 extends 1 & Props ? {} : Props} MDXContentProps */', + '/** @typedef {(0 extends 1 & Props ? {} : Props) & {components?: {}}} MDXContentProps */', '' ) }, @@ -1198,6 +1298,19 @@ test('create virtual code w/ MDX layout in case of a default export preceded by ' * 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 MDXLayout} */', + ' MDXLayout,', + ' /** {@link named} */', + ' named', + ' }', ' return <>>', '}', '', @@ -1212,7 +1325,7 @@ test('create virtual code w/ MDX layout in case of a default export preceded by '}', '', '// @ts-ignore', - '/** @typedef {0 extends 1 & Props ? {} : Props} MDXContentProps */', + '/** @typedef {(0 extends 1 & Props ? {} : Props) & {components?: {}}} MDXContentProps */', '' ) }, @@ -1275,7 +1388,7 @@ test('create virtual code w/ mdxFlowExpression', () => { mappings: [ { sourceOffsets: [0], - generatedOffsets: [322], + generatedOffsets: [568], lengths: [9], data: { completion: true, @@ -1299,6 +1412,15 @@ test('create virtual code w/ mdxFlowExpression', () => { ' * 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', + ' }', ' return <>{Math.PI}>', '}', '', @@ -1313,7 +1435,7 @@ test('create virtual code w/ mdxFlowExpression', () => { '}', '', '// @ts-ignore', - '/** @typedef {0 extends 1 & Props ? {} : Props} MDXContentProps */', + '/** @typedef {(0 extends 1 & Props ? {} : Props) & {components?: {}}} MDXContentProps */', '' ) }, @@ -1345,11 +1467,23 @@ test('create virtual code w/ mdxJsxFlowElement w/ children', () => { const plugin = createMdxLanguagePlugin() const snapshot = snapshotFromLines( + 'export function Local() {}', + '', '