diff --git a/__tests__/lib/exports/index.test.ts b/__tests__/lib/exports/index.test.ts new file mode 100644 index 000000000..df73fb068 --- /dev/null +++ b/__tests__/lib/exports/index.test.ts @@ -0,0 +1,19 @@ +import singleExportMdx from './input/singleExport.mdx?raw'; +import multipleExportsMdx from './input/multipleExports.mdx?raw'; +import weirdExportsMdx from './input/weirdExports.mdx?raw'; +import { exports } from '../../../lib'; + +describe('export tags', () => { + it('returns a single export name', () => { + + expect(exports(singleExportMdx)).toStrictEqual(['Foo']); + }); + it('returns multiple export names', () => { + + expect(exports(multipleExportsMdx)).toStrictEqual(['Foo', 'Bar']); + }); + it('returns different types of export names', () => { + + expect(exports(weirdExportsMdx)).toStrictEqual(['Foo', 'bar', 'doSomethingFunction', 'YELLING']); + }); +}); \ No newline at end of file diff --git a/__tests__/lib/exports/input/multipleExports.mdx b/__tests__/lib/exports/input/multipleExports.mdx new file mode 100644 index 000000000..42ecc9b8d --- /dev/null +++ b/__tests__/lib/exports/input/multipleExports.mdx @@ -0,0 +1,12 @@ +import React from 'react'; + +export const Foo = () => { + return
Hello World
; +} + +export const Bar = () => { + return ; +} + +## Hey there + \ No newline at end of file diff --git a/__tests__/lib/exports/input/singleExport.mdx b/__tests__/lib/exports/input/singleExport.mdx new file mode 100644 index 000000000..e75889939 --- /dev/null +++ b/__tests__/lib/exports/input/singleExport.mdx @@ -0,0 +1,8 @@ +import React from 'react'; + +export const Foo = () => { + return
Hello World
; +} + +## Hey there + \ No newline at end of file diff --git a/__tests__/lib/exports/input/weirdExports.mdx b/__tests__/lib/exports/input/weirdExports.mdx new file mode 100644 index 000000000..3953dd587 --- /dev/null +++ b/__tests__/lib/exports/input/weirdExports.mdx @@ -0,0 +1,19 @@ +import React from 'react'; + +export function Foo() { + return
Hello World
; +} + +export const bar = () => { + return ; +} + +export function doSomethingFunction(input) { + return input.trim(); +} + +export const +YELLING = () => {} + +## Hey there + \ No newline at end of file diff --git a/__tests__/lib/hast.test.ts b/__tests__/lib/hast.test.ts index 3cf95bd71..1cade7566 100644 --- a/__tests__/lib/hast.test.ts +++ b/__tests__/lib/hast.test.ts @@ -1,4 +1,4 @@ -import { hast, hastFromHtml } from '../../lib'; +import { hast } from '../../lib'; import { h } from 'hastscript'; describe('hast transformer', () => { @@ -19,6 +19,7 @@ describe('hast transformer', () => { h('h2', { id: 'its-coming-from-within-the-component' }, "It's coming from within the component!"), ); + // @ts-ignore expect(hast(md, { components })).toStrictEqualExceptPosition(expected); }); }); diff --git a/__tests__/lib/mdast/__snapshots__/index.test.ts.snap b/__tests__/lib/mdast/__snapshots__/index.test.ts.snap new file mode 100644 index 000000000..596918126 --- /dev/null +++ b/__tests__/lib/mdast/__snapshots__/index.test.ts.snap @@ -0,0 +1,544 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`mdast transformer > parses exports 1`] = ` +{ + "children": [ + { + "data": { + "estree": Node { + "body": [ + Node { + "end": 26, + "loc": { + "end": { + "column": 26, + "line": 1, + "offset": 26, + }, + "start": { + "column": 0, + "line": 1, + "offset": 0, + }, + }, + "range": [ + 0, + 26, + ], + "source": Node { + "end": 25, + "loc": { + "end": { + "column": 25, + "line": 1, + "offset": 25, + }, + "start": { + "column": 18, + "line": 1, + "offset": 18, + }, + }, + "range": [ + 18, + 25, + ], + "raw": ""react"", + "start": 18, + "type": "Literal", + "value": "react", + }, + "specifiers": [ + Node { + "end": 12, + "loc": { + "end": { + "column": 12, + "line": 1, + "offset": 12, + }, + "start": { + "column": 7, + "line": 1, + "offset": 7, + }, + }, + "local": Node { + "end": 12, + "loc": { + "end": { + "column": 12, + "line": 1, + "offset": 12, + }, + "start": { + "column": 7, + "line": 1, + "offset": 7, + }, + }, + "name": "React", + "range": [ + 7, + 12, + ], + "start": 7, + "type": "Identifier", + }, + "range": [ + 7, + 12, + ], + "start": 7, + "type": "ImportDefaultSpecifier", + }, + ], + "start": 0, + "type": "ImportDeclaration", + }, + ], + "comments": [], + "end": 26, + "loc": { + "end": { + "column": 26, + "line": 1, + "offset": 26, + }, + "start": { + "column": 0, + "line": 1, + "offset": 0, + }, + }, + "range": [ + 0, + 26, + ], + "sourceType": "module", + "start": 0, + "type": "Program", + }, + }, + "position": { + "end": { + "column": 27, + "line": 1, + "offset": 26, + }, + "start": { + "column": 1, + "line": 1, + "offset": 0, + }, + }, + "type": "mdxjsEsm", + "value": "import React from "react";", + }, + { + "data": { + "estree": Node { + "body": [ + Node { + "declaration": Node { + "declarations": [ + Node { + "end": 89, + "id": Node { + "end": 44, + "loc": { + "end": { + "column": 16, + "line": 3, + "offset": 44, + }, + "start": { + "column": 13, + "line": 3, + "offset": 41, + }, + }, + "name": "Foo", + "range": [ + 41, + 44, + ], + "start": 41, + "type": "Identifier", + }, + "init": Node { + "async": false, + "body": Node { + "body": [ + Node { + "argument": Node { + "children": [ + Node { + "end": 80, + "loc": { + "end": { + "column": 25, + "line": 4, + "offset": 80, + }, + "start": { + "column": 14, + "line": 4, + "offset": 69, + }, + }, + "range": [ + 69, + 80, + ], + "raw": "Hello World", + "start": 69, + "type": "JSXText", + "value": "Hello World", + }, + ], + "closingElement": Node { + "end": 86, + "loc": { + "end": { + "column": 31, + "line": 4, + "offset": 86, + }, + "start": { + "column": 25, + "line": 4, + "offset": 80, + }, + }, + "name": Node { + "end": 85, + "loc": { + "end": { + "column": 30, + "line": 4, + "offset": 85, + }, + "start": { + "column": 27, + "line": 4, + "offset": 82, + }, + }, + "name": "div", + "range": [ + 82, + 85, + ], + "start": 82, + "type": "JSXIdentifier", + }, + "range": [ + 80, + 86, + ], + "start": 80, + "type": "JSXClosingElement", + }, + "end": 86, + "loc": { + "end": { + "column": 31, + "line": 4, + "offset": 86, + }, + "start": { + "column": 9, + "line": 4, + "offset": 64, + }, + }, + "openingElement": Node { + "attributes": [], + "end": 69, + "loc": { + "end": { + "column": 14, + "line": 4, + "offset": 69, + }, + "start": { + "column": 9, + "line": 4, + "offset": 64, + }, + }, + "name": Node { + "end": 68, + "loc": { + "end": { + "column": 13, + "line": 4, + "offset": 68, + }, + "start": { + "column": 10, + "line": 4, + "offset": 65, + }, + }, + "name": "div", + "range": [ + 65, + 68, + ], + "start": 65, + "type": "JSXIdentifier", + }, + "range": [ + 64, + 69, + ], + "selfClosing": false, + "start": 64, + "type": "JSXOpeningElement", + }, + "range": [ + 64, + 86, + ], + "start": 64, + "type": "JSXElement", + }, + "end": 87, + "loc": { + "end": { + "column": 32, + "line": 4, + "offset": 87, + }, + "start": { + "column": 2, + "line": 4, + "offset": 57, + }, + }, + "range": [ + 57, + 87, + ], + "start": 57, + "type": "ReturnStatement", + }, + ], + "end": 89, + "loc": { + "end": { + "column": 1, + "line": 5, + "offset": 89, + }, + "start": { + "column": 25, + "line": 3, + "offset": 53, + }, + }, + "range": [ + 53, + 89, + ], + "start": 53, + "type": "BlockStatement", + }, + "end": 89, + "expression": false, + "generator": false, + "id": null, + "loc": { + "end": { + "column": 1, + "line": 5, + "offset": 89, + }, + "start": { + "column": 19, + "line": 3, + "offset": 47, + }, + }, + "params": [], + "range": [ + 47, + 89, + ], + "start": 47, + "type": "ArrowFunctionExpression", + }, + "loc": { + "end": { + "column": 1, + "line": 5, + "offset": 89, + }, + "start": { + "column": 13, + "line": 3, + "offset": 41, + }, + }, + "range": [ + 41, + 89, + ], + "start": 41, + "type": "VariableDeclarator", + }, + ], + "end": 89, + "kind": "const", + "loc": { + "end": { + "column": 1, + "line": 5, + "offset": 89, + }, + "start": { + "column": 7, + "line": 3, + "offset": 35, + }, + }, + "range": [ + 35, + 89, + ], + "start": 35, + "type": "VariableDeclaration", + }, + "end": 89, + "loc": { + "end": { + "column": 1, + "line": 5, + "offset": 89, + }, + "start": { + "column": 0, + "line": 3, + "offset": 28, + }, + }, + "range": [ + 28, + 89, + ], + "source": null, + "specifiers": [], + "start": 28, + "type": "ExportNamedDeclaration", + }, + ], + "comments": [], + "end": 89, + "loc": { + "end": { + "column": 1, + "line": 5, + "offset": 89, + }, + "start": { + "column": 0, + "line": 3, + "offset": 28, + }, + }, + "range": [ + 28, + 89, + ], + "sourceType": "module", + "start": 28, + "type": "Program", + }, + }, + "position": { + "end": { + "column": 2, + "line": 5, + "offset": 89, + }, + "start": { + "column": 1, + "line": 3, + "offset": 28, + }, + }, + "type": "mdxjsEsm", + "value": "export const Foo = () => { + return
Hello World
; +}", + }, + { + "children": [ + { + "position": { + "end": { + "column": 13, + "line": 7, + "offset": 103, + }, + "start": { + "column": 4, + "line": 7, + "offset": 94, + }, + }, + "type": "text", + "value": "Hey there", + }, + ], + "depth": 2, + "position": { + "end": { + "column": 14, + "line": 7, + "offset": 104, + }, + "start": { + "column": 1, + "line": 7, + "offset": 91, + }, + }, + "type": "heading", + }, + { + "attributes": [], + "children": [], + "name": "Foo", + "position": { + "end": { + "column": 8, + "line": 8, + "offset": 112, + }, + "start": { + "column": 1, + "line": 8, + "offset": 105, + }, + }, + "type": "mdxJsxFlowElement", + }, + ], + "position": { + "end": { + "column": 8, + "line": 8, + "offset": 112, + }, + "start": { + "column": 1, + "line": 1, + "offset": 0, + }, + }, + "type": "root", +} +`; diff --git a/__tests__/lib/mdast/esm/in.mdx b/__tests__/lib/mdast/esm/in.mdx new file mode 100644 index 000000000..e75889939 --- /dev/null +++ b/__tests__/lib/mdast/esm/in.mdx @@ -0,0 +1,8 @@ +import React from 'react'; + +export const Foo = () => { + return
Hello World
; +} + +## Hey there + \ No newline at end of file diff --git a/__tests__/lib/mdast/esm/out.json b/__tests__/lib/mdast/esm/out.json new file mode 100644 index 000000000..bfabd053a --- /dev/null +++ b/__tests__/lib/mdast/esm/out.json @@ -0,0 +1,538 @@ +{ + "type": "root", + "children": [ + { + "type": "mdxjsEsm", + "value": "import React from 'react';", + "position": { + "start": { + "line": 1, + "column": 1, + "offset": 0 + }, + "end": { + "line": 1, + "column": 27, + "offset": 26 + } + }, + "data": { + "estree": { + "type": "Program", + "start": 0, + "end": 26, + "loc": { + "start": { + "line": 1, + "column": 0, + "offset": 0 + }, + "end": { + "line": 1, + "column": 26, + "offset": 26 + } + }, + "body": [ + { + "type": "ImportDeclaration", + "start": 0, + "end": 26, + "loc": { + "start": { + "line": 1, + "column": 0, + "offset": 0 + }, + "end": { + "line": 1, + "column": 26, + "offset": 26 + } + }, + "specifiers": [ + { + "type": "ImportDefaultSpecifier", + "start": 7, + "end": 12, + "loc": { + "start": { + "line": 1, + "column": 7, + "offset": 7 + }, + "end": { + "line": 1, + "column": 12, + "offset": 12 + } + }, + "local": { + "type": "Identifier", + "start": 7, + "end": 12, + "loc": { + "start": { + "line": 1, + "column": 7, + "offset": 7 + }, + "end": { + "line": 1, + "column": 12, + "offset": 12 + } + }, + "name": "React", + "range": [ + 7, + 12 + ] + }, + "range": [ + 7, + 12 + ] + } + ], + "source": { + "type": "Literal", + "start": 18, + "end": 25, + "loc": { + "start": { + "line": 1, + "column": 18, + "offset": 18 + }, + "end": { + "line": 1, + "column": 25, + "offset": 25 + } + }, + "value": "react", + "raw": "'react'", + "range": [ + 18, + 25 + ] + }, + "range": [ + 0, + 26 + ] + } + ], + "sourceType": "module", + "comments": [], + "range": [ + 0, + 26 + ] + } + } + }, + { + "type": "mdxjsEsm", + "value": "export const Foo = () => {\n return
Hello World
;\n}", + "position": { + "start": { + "line": 3, + "column": 1, + "offset": 28 + }, + "end": { + "line": 5, + "column": 2, + "offset": 89 + } + }, + "data": { + "estree": { + "type": "Program", + "start": 28, + "end": 89, + "loc": { + "start": { + "line": 3, + "column": 0, + "offset": 28 + }, + "end": { + "line": 5, + "column": 1, + "offset": 89 + } + }, + "body": [ + { + "type": "ExportNamedDeclaration", + "start": 28, + "end": 89, + "loc": { + "start": { + "line": 3, + "column": 0, + "offset": 28 + }, + "end": { + "line": 5, + "column": 1, + "offset": 89 + } + }, + "declaration": { + "type": "VariableDeclaration", + "start": 35, + "end": 89, + "loc": { + "start": { + "line": 3, + "column": 7, + "offset": 35 + }, + "end": { + "line": 5, + "column": 1, + "offset": 89 + } + }, + "declarations": [ + { + "type": "VariableDeclarator", + "start": 41, + "end": 89, + "loc": { + "start": { + "line": 3, + "column": 13, + "offset": 41 + }, + "end": { + "line": 5, + "column": 1, + "offset": 89 + } + }, + "id": { + "type": "Identifier", + "start": 41, + "end": 44, + "loc": { + "start": { + "line": 3, + "column": 13, + "offset": 41 + }, + "end": { + "line": 3, + "column": 16, + "offset": 44 + } + }, + "name": "Foo", + "range": [ + 41, + 44 + ] + }, + "init": { + "type": "ArrowFunctionExpression", + "start": 47, + "end": 89, + "loc": { + "start": { + "line": 3, + "column": 19, + "offset": 47 + }, + "end": { + "line": 5, + "column": 1, + "offset": 89 + } + }, + "id": null, + "expression": false, + "generator": false, + "async": false, + "params": [], + "body": { + "type": "BlockStatement", + "start": 53, + "end": 89, + "loc": { + "start": { + "line": 3, + "column": 25, + "offset": 53 + }, + "end": { + "line": 5, + "column": 1, + "offset": 89 + } + }, + "body": [ + { + "type": "ReturnStatement", + "start": 57, + "end": 87, + "loc": { + "start": { + "line": 4, + "column": 2, + "offset": 57 + }, + "end": { + "line": 4, + "column": 32, + "offset": 87 + } + }, + "argument": { + "type": "JSXElement", + "start": 64, + "end": 86, + "loc": { + "start": { + "line": 4, + "column": 9, + "offset": 64 + }, + "end": { + "line": 4, + "column": 31, + "offset": 86 + } + }, + "openingElement": { + "type": "JSXOpeningElement", + "start": 64, + "end": 69, + "loc": { + "start": { + "line": 4, + "column": 9, + "offset": 64 + }, + "end": { + "line": 4, + "column": 14, + "offset": 69 + } + }, + "attributes": [], + "name": { + "type": "JSXIdentifier", + "start": 65, + "end": 68, + "loc": { + "start": { + "line": 4, + "column": 10, + "offset": 65 + }, + "end": { + "line": 4, + "column": 13, + "offset": 68 + } + }, + "name": "div", + "range": [ + 65, + 68 + ] + }, + "selfClosing": false, + "range": [ + 64, + 69 + ] + }, + "closingElement": { + "type": "JSXClosingElement", + "start": 80, + "end": 86, + "loc": { + "start": { + "line": 4, + "column": 25, + "offset": 80 + }, + "end": { + "line": 4, + "column": 31, + "offset": 86 + } + }, + "name": { + "type": "JSXIdentifier", + "start": 82, + "end": 85, + "loc": { + "start": { + "line": 4, + "column": 27, + "offset": 82 + }, + "end": { + "line": 4, + "column": 30, + "offset": 85 + } + }, + "name": "div", + "range": [ + 82, + 85 + ] + }, + "range": [ + 80, + 86 + ] + }, + "children": [ + { + "type": "JSXText", + "start": 69, + "end": 80, + "loc": { + "start": { + "line": 4, + "column": 14, + "offset": 69 + }, + "end": { + "line": 4, + "column": 25, + "offset": 80 + } + }, + "value": "Hello World", + "raw": "Hello World", + "range": [ + 69, + 80 + ] + } + ], + "range": [ + 64, + 86 + ] + }, + "range": [ + 57, + 87 + ] + } + ], + "range": [ + 53, + 89 + ] + }, + "range": [ + 47, + 89 + ] + }, + "range": [ + 41, + 89 + ] + } + ], + "kind": "const", + "range": [ + 35, + 89 + ] + }, + "specifiers": [], + "source": null, + "range": [ + 28, + 89 + ] + } + ], + "sourceType": "module", + "comments": [], + "range": [ + 28, + 89 + ] + } + } + }, + { + "type": "heading", + "depth": 2, + "children": [ + { + "type": "text", + "value": "Hey there", + "position": { + "start": { + "line": 7, + "column": 4, + "offset": 94 + }, + "end": { + "line": 7, + "column": 13, + "offset": 103 + } + } + } + ], + "position": { + "start": { + "line": 7, + "column": 1, + "offset": 91 + }, + "end": { + "line": 7, + "column": 14, + "offset": 104 + } + } + }, + { + "type": "mdxJsxFlowElement", + "name": "Foo", + "attributes": [], + "children": [], + "position": { + "start": { + "line": 8, + "column": 1, + "offset": 105 + }, + "end": { + "line": 8, + "column": 8, + "offset": 112 + } + } + } + ], + "position": { + "start": { + "line": 1, + "column": 1, + "offset": 0 + }, + "end": { + "line": 8, + "column": 8, + "offset": 112 + } + } +} diff --git a/__tests__/lib/mdast/index.test.ts b/__tests__/lib/mdast/index.test.ts index 43ca8519b..572579486 100644 --- a/__tests__/lib/mdast/index.test.ts +++ b/__tests__/lib/mdast/index.test.ts @@ -15,6 +15,9 @@ import variablesWithSpacesJson from './variables-with-spaces/out.json'; import inlineImagesMdx from './images/inline/in.mdx?raw'; import inlineImagesJson from './images/inline/out.json'; +import esmMdx from './esm/in.mdx?raw'; +import esmJson from './esm/out.json'; + describe('mdast transformer', async () => { it('parses null attributes', () => { // @ts-ignore @@ -40,4 +43,9 @@ describe('mdast transformer', async () => { // @ts-ignore expect(mdast(inlineImagesMdx)).toStrictEqualExceptPosition(inlineImagesJson); }); + + it('parses esm (imports and exports)', () => { + // @ts-ignore + expect(mdast(esmMdx)).toStrictEqualExceptPosition(esmJson); + }); }); diff --git a/lib/exports.ts b/lib/exports.ts new file mode 100644 index 000000000..ff072028b --- /dev/null +++ b/lib/exports.ts @@ -0,0 +1,21 @@ +import { visit } from 'unist-util-visit'; +import mdast from './mdast'; +import { isMDXEsm } from '../processor/utils'; +import { MdxjsEsm } from 'mdast-util-mdx'; + +const EXPORT_NAME_REGEX = /export\s+(?:const|let|var|function)\s+(\w+)/; + +const exports = (doc: string) => { + const set = new Set(); + + visit(mdast(doc), isMDXEsm, (node: MdxjsEsm) => { + if (node.value?.match(EXPORT_NAME_REGEX)) { + const [, name] = node.value.match(EXPORT_NAME_REGEX); + set.add(name); + } + }); + + return Array.from(set); +}; + +export default exports; diff --git a/lib/index.ts b/lib/index.ts index 17e935e51..e56289ba1 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -2,11 +2,12 @@ export type { MdastOpts } from './ast-processor'; export { default as astProcessor, remarkPlugins } from './ast-processor'; export { default as compile } from './compile'; +export { default as exports } from './exports'; export { default as hast } from './hast'; export { default as mdast } from './mdast'; export { default as mdastV6 } from './mdastV6'; export { default as mdx } from './mdx'; +export { default as migrate } from './migrate'; export { default as plain } from './plain'; export { default as run } from './run'; export { default as tags } from './tags'; -export { default as migrate } from './migrate'; diff --git a/lib/tags.ts b/lib/tags.ts index 73dec329a..9cf094e50 100644 --- a/lib/tags.ts +++ b/lib/tags.ts @@ -7,7 +7,7 @@ const tags = (doc: string) => { const set = new Set(); visit(mdast(doc), isMDXElement, (node: MdxJsxFlowElement | MdxJsxTextElement) => { - if (node.name.match(/^[A-Z]/)) { + if (node.name?.match(/^[A-Z]/)) { set.add(node.name); } }); diff --git a/processor/utils.ts b/processor/utils.ts index 54c9e3e99..981396e1c 100644 --- a/processor/utils.ts +++ b/processor/utils.ts @@ -1,5 +1,5 @@ import type { Node } from 'mdast'; -import type { MdxJsxFlowElement, MdxJsxTextElement, MdxFlowExpression } from 'mdast-util-mdx'; +import type { MdxJsxFlowElement, MdxJsxTextElement, MdxFlowExpression, MdxjsEsm } from 'mdast-util-mdx'; import type { MdxJsxAttribute, MdxJsxAttributeValueExpression, @@ -101,12 +101,22 @@ export const getChildren = (jsx: MdxJsxFlowElement | MdxJsxTextElement): any * TODO: Make this more extensible to all types of nodes. isElement(node, 'type' or ['type1', 'type2']), say * * @param {Node} node - * @returns {(node is MdxJsxFlowElement | MdxJsxTextElement)} + * @returns {(node is MdxJsxFlowElement | MdxJsxTextElement | MdxjsEsm)} */ export const isMDXElement = (node: Node): node is MdxJsxFlowElement | MdxJsxTextElement => { return ['mdxJsxFlowElement', 'mdxJsxTextElement'].includes(node.type); }; +/** + * Tests if a node is an MDX ESM element (i.e. import or export). + * + * @param {Node} node + * @returns {boolean} + */ +export const isMDXEsm = (node: Node): node is MdxjsEsm => { + return node.type === 'mdxjsEsm'; +} + /** * Takes an HTML string and formats it for display in the editor. Removes leading/trailing newlines * and unindents the HTML.