0
Fork 0
mirror of https://github.com/withastro/astro.git synced 2024-12-23 21:53:55 -05:00
astro/packages/integrations/mdx/src/index.ts
Ben Holmes f1a52c18af
[MDX] Switch from Shiki Twoslash -> Astro Markdown highlighter (#4292)
* freat: twoslash -> Astro shiki parser

* test: update shiki style check

* feat: always apply rehypeRaw

* deps: move remark-shiki-twoslash to dev

* test: add shiki-twoslash test

* docs: update readme with twoslash example

* chore: changeset

* nit: remove "describe('disabled')"
2022-08-15 10:43:12 -04:00

160 lines
5.6 KiB
TypeScript

import { compile as mdxCompile, nodeTypes } from '@mdx-js/mdx';
import mdxPlugin, { Options as MdxRollupPluginOptions } from '@mdx-js/rollup';
import type { AstroConfig, AstroIntegration } from 'astro';
import { parse as parseESM } from 'es-module-lexer';
import rehypeRaw from 'rehype-raw';
import remarkGfm from 'remark-gfm';
import remarkSmartypants from 'remark-smartypants';
import { VFile } from 'vfile';
import type { Plugin as VitePlugin } from 'vite';
import { rehypeApplyFrontmatterExport, remarkInitializeAstroData } from './astro-data-utils.js';
import rehypeCollectHeadings from './rehype-collect-headings.js';
import remarkPrism from './remark-prism.js';
import remarkShiki from './remark-shiki.js';
import { getFileInfo, parseFrontmatter } from './utils.js';
type WithExtends<T> = T | { extends: T };
type MdxOptions = {
remarkPlugins?: WithExtends<MdxRollupPluginOptions['remarkPlugins']>;
rehypePlugins?: WithExtends<MdxRollupPluginOptions['rehypePlugins']>;
};
const DEFAULT_REMARK_PLUGINS: MdxRollupPluginOptions['remarkPlugins'] = [
remarkGfm,
remarkSmartypants,
];
const DEFAULT_REHYPE_PLUGINS: MdxRollupPluginOptions['rehypePlugins'] = [];
const RAW_CONTENT_ERROR =
'MDX does not support rawContent()! If you need to read the Markdown contents to calculate values (ex. reading time), we suggest injecting frontmatter via remark plugins. Learn more on our docs: https://docs.astro.build/en/guides/integrations-guide/mdx/#inject-frontmatter-via-remark-or-rehype-plugins';
const COMPILED_CONTENT_ERROR =
'MDX does not support compiledContent()! If you need to read the HTML contents to calculate values (ex. reading time), we suggest injecting frontmatter via rehype plugins. Learn more on our docs: https://docs.astro.build/en/guides/integrations-guide/mdx/#inject-frontmatter-via-remark-or-rehype-plugins';
function handleExtends<T>(config: WithExtends<T[] | undefined>, defaults: T[] = []): T[] {
if (Array.isArray(config)) return config;
return [...defaults, ...(config?.extends ?? [])];
}
async function getRemarkPlugins(
mdxOptions: MdxOptions,
config: AstroConfig
): Promise<MdxRollupPluginOptions['remarkPlugins']> {
let remarkPlugins = [
// Initialize vfile.data.astroExports before all plugins are run
remarkInitializeAstroData,
...handleExtends(mdxOptions.remarkPlugins, DEFAULT_REMARK_PLUGINS),
];
if (config.markdown.syntaxHighlight === 'shiki') {
remarkPlugins.push([await remarkShiki(config.markdown.shikiConfig)]);
}
if (config.markdown.syntaxHighlight === 'prism') {
remarkPlugins.push(remarkPrism);
}
return remarkPlugins;
}
function getRehypePlugins(
mdxOptions: MdxOptions,
config: AstroConfig
): MdxRollupPluginOptions['rehypePlugins'] {
let rehypePlugins = [
[rehypeRaw, { passThrough: nodeTypes }] as any,
...handleExtends(mdxOptions.rehypePlugins, DEFAULT_REHYPE_PLUGINS),
];
// getHeadings() is guaranteed by TS, so we can't allow user to override
rehypePlugins.unshift(rehypeCollectHeadings);
return rehypePlugins;
}
export default function mdx(mdxOptions: MdxOptions = {}): AstroIntegration {
return {
name: '@astrojs/mdx',
hooks: {
'astro:config:setup': async ({ updateConfig, config, addPageExtension, command }: any) => {
addPageExtension('.mdx');
const mdxPluginOpts: MdxRollupPluginOptions = {
remarkPlugins: await getRemarkPlugins(mdxOptions, config),
rehypePlugins: getRehypePlugins(mdxOptions, config),
jsx: true,
jsxImportSource: 'astro',
// Note: disable `.md` support
format: 'mdx',
mdExtensions: [],
};
updateConfig({
vite: {
plugins: [
{
enforce: 'pre',
...mdxPlugin(mdxPluginOpts),
// Override transform to alter code before MDX compilation
// ex. inject layouts
async transform(code, id) {
if (!id.endsWith('mdx')) return;
const { data: frontmatter, content: pageContent } = parseFrontmatter(code, id);
const compiled = await mdxCompile(new VFile({ value: pageContent, path: id }), {
...mdxPluginOpts,
rehypePlugins: [
...(mdxPluginOpts.rehypePlugins ?? []),
() => rehypeApplyFrontmatterExport(frontmatter),
],
});
return {
code: String(compiled.value),
map: compiled.map,
};
},
},
{
name: '@astrojs/mdx-postprocess',
// These transforms must happen *after* JSX runtime transformations
transform(code, id) {
if (!id.endsWith('.mdx')) return;
const [, moduleExports] = parseESM(code);
const { fileUrl, fileId } = getFileInfo(id, config);
if (!moduleExports.includes('url')) {
code += `\nexport const url = ${JSON.stringify(fileUrl)};`;
}
if (!moduleExports.includes('file')) {
code += `\nexport const file = ${JSON.stringify(fileId)};`;
}
if (!moduleExports.includes('rawContent')) {
code += `\nexport function rawContent() { throw new Error(${JSON.stringify(
RAW_CONTENT_ERROR
)}) };`;
}
if (!moduleExports.includes('compiledContent')) {
code += `\nexport function compiledContent() { throw new Error(${JSON.stringify(
COMPILED_CONTENT_ERROR
)}) };`;
}
if (!moduleExports.includes('Content')) {
code += `\nexport const Content = MDXContent;`;
}
if (command === 'dev') {
// TODO: decline HMR updates until we have a stable approach
code += `\nif (import.meta.hot) {
import.meta.hot.decline();
}`;
}
return code;
},
},
] as VitePlugin[],
},
});
},
},
};
}