1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
|
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 remarkShikiTwoslash from 'remark-shiki-twoslash';
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 { 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'] = [];
function handleExtends<T>(config: WithExtends<T[] | undefined>, defaults: T[] = []): T[] {
if (Array.isArray(config)) return config;
return [...defaults, ...(config?.extends ?? [])];
}
function getRemarkPlugins(
mdxOptions: MdxOptions,
config: AstroConfig
): 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') {
// Default export still requires ".default" chaining for some reason
// Workarounds tried:
// - "import * as remarkShikiTwoslash"
// - "import { default as remarkShikiTwoslash }"
const shikiTwoslash = (remarkShikiTwoslash as any).default ?? remarkShikiTwoslash;
remarkPlugins.push([shikiTwoslash, config.markdown.shikiConfig]);
}
if (config.markdown.syntaxHighlight === 'prism') {
remarkPlugins.push(remarkPrism);
}
return remarkPlugins;
}
function getRehypePlugins(
mdxOptions: MdxOptions,
config: AstroConfig
): MdxRollupPluginOptions['rehypePlugins'] {
let rehypePlugins = handleExtends(mdxOptions.rehypePlugins, DEFAULT_REHYPE_PLUGINS);
if (config.markdown.syntaxHighlight === 'shiki' || config.markdown.syntaxHighlight === 'prism') {
rehypePlugins.unshift([rehypeRaw, { passThrough: nodeTypes }]);
}
// 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': ({ updateConfig, config, addPageExtension, command }: any) => {
addPageExtension('.mdx');
const mdxPluginOpts: MdxRollupPluginOptions = {
remarkPlugins: 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 (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[],
},
});
},
},
};
}
|