diff --git a/package.json b/package.json index ef945a35..ff26c65c 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ ], "rules": { "@typescript-eslint/prefer-nullish-coalescing": "off", + "max-depth": "off", "max-params": "off" } } diff --git a/packages/language-service/lib/language-module.js b/packages/language-service/lib/language-module.js index d2d11bcf..ad5275cd 100644 --- a/packages/language-service/lib/language-module.js +++ b/packages/language-service/lib/language-module.js @@ -16,23 +16,40 @@ import remarkMdx from 'remark-mdx' import remarkParse from 'remark-parse' import {unified} from 'unified' -const componentStart = ` +const layoutJsDoc = ` +/** @typedef {MDXContentProps & { children: JSX.Element }} MDXLayoutProps */ + +/** + * There is one special component: [MDX layout](https://mdxjs.com/docs/using-mdx/#layout). + * If it is defined, it’s used to wrap all content. + * A layout can be defined from within MDX using a default export. + * + * @param {{readonly [K in keyof MDXLayoutProps]: MDXLayoutProps[K]}} props + * The [props](https://mdxjs.com/docs/using-mdx/#props) that have been passed to the MDX component. + * In addition, the MDX layout receives the \`children\` prop, which contains the rendered MDX content. + */ +` + +/** + * @param {boolean} hasLayout + */ +const componentStart = (hasLayout) => ` /** * Render the MDX contents. * - * @param {MDXContentProps} props - * The props that have been passed to the MDX component. + * @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) { +${hasLayout ? '' : 'export default '}function MDXContent(props) { return ` const componentEnd = ` } // @ts-ignore -/** @typedef {Props} MDXContentProps */ +/** @typedef {0 extends 1 & Props ? {} : Props} MDXContentProps */ ` -const fallback = componentStart + componentEnd +const fallback = componentStart(false) + componentEnd /** * Visit an mdast tree with and enter and exit callback. @@ -165,6 +182,7 @@ function getVirtualFiles(fileName, snapshot, ts, processor) { /** @type {VirtualFile[]} */ const virtualFiles = [] + let hasLayout = false let esm = '' let jsx = '' let markdown = '' @@ -250,6 +268,27 @@ function getVirtualFiles(fileName, snapshot, ts, processor) { case 'mdxjsEsm': { updateMarkdownFromNode(node) + const body = node.data?.estree?.body + + if (body) { + for (const child of body) { + if (child.type === 'ExportNamedDeclaration') { + for (const specifier of child.specifiers) { + if (specifier.local.name === 'default') { + hasLayout = true + break + } + } + } + + if (child.type === 'ExportDefaultDeclaration') { + esm += layoutJsDoc + hasLayout = true + break + } + } + } + addOffset(esmMapping, start, esm.length, end - start) esm += mdx.slice(start, end) + '\n' break @@ -334,7 +373,7 @@ function getVirtualFiles(fileName, snapshot, ts, processor) { ) updateMarkdownFromOffsets(mdx.length, mdx.length) - esm += componentStart + esm += componentStart(hasLayout) for (let i = 0; i < jsxMapping.generatedOffsets.length; i++) { jsxMapping.generatedOffsets[i] += esm.length